From 7d025ed852f777b93ead2066dfcbc07318f65d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernani=20S=C3=A3o=20Thiago?= Date: Fri, 12 Apr 2024 14:41:26 -0600 Subject: [PATCH] ON-670: Refactor ERC-7432 (#27) ON-670: Refactor ERC-7432 --- contracts/ERC7432/NftRolesRegistryVault.sol | 225 +++++ contracts/RolesRegistry.sol | 213 ----- contracts/interfaces/IERC7432.sol | 149 ++-- .../interfaces/IERC7432VaultExtension.sol | 24 + test/ERC7432/NftRolesRegistryVault.ts | 370 +++++++++ test/ERC7432/mockData.ts | 32 + test/RolesRegistry.spec.ts | 781 ------------------ .../SftRolesRegistry/SftRolesRegistry.spec.ts | 4 +- .../SftRolesRegistrySingleRole.spec.ts | 4 +- test/SftRolesRegistry/helpers/assertEvents.ts | 3 +- test/SftRolesRegistry/helpers/mockData.ts | 62 +- test/SftRolesRegistry/types.ts | 22 - test/helpers.ts | 19 +- test/types.ts | 25 +- 14 files changed, 730 insertions(+), 1203 deletions(-) create mode 100644 contracts/ERC7432/NftRolesRegistryVault.sol delete mode 100644 contracts/RolesRegistry.sol create mode 100644 contracts/interfaces/IERC7432VaultExtension.sol create mode 100644 test/ERC7432/NftRolesRegistryVault.ts create mode 100644 test/ERC7432/mockData.ts delete mode 100644 test/RolesRegistry.spec.ts diff --git a/contracts/ERC7432/NftRolesRegistryVault.sol b/contracts/ERC7432/NftRolesRegistryVault.sol new file mode 100644 index 0000000..f9f30a3 --- /dev/null +++ b/contracts/ERC7432/NftRolesRegistryVault.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +import { IERC7432 } from '../interfaces/IERC7432.sol'; +import { IERC7432VaultExtension } from '../interfaces/IERC7432VaultExtension.sol'; +import { IERC721 } from '@openzeppelin/contracts/token/ERC721/IERC721.sol'; + +contract NftRolesRegistryVault is IERC7432, IERC7432VaultExtension { + struct RoleData { + address recipient; + uint64 expirationDate; + bool revocable; + bytes data; + } + + // tokenAddress => tokenId => owner + mapping(address => mapping(uint256 => address)) public originalOwners; + + // tokenAddress => tokenId => roleId => struct(recipient, expirationDate, revocable, data) + mapping(address => mapping(uint256 => mapping(bytes32 => RoleData))) public roles; + + // owner => tokenAddress => operator => isApproved + mapping(address => mapping(address => mapping(address => bool))) public tokenApprovals; + + /** ERC-7432 External Functions **/ + + function grantRole(IERC7432.Role calldata _role) external override { + require(_role.expirationDate > block.timestamp, 'NftRolesRegistryVault: expiration date must be in the future'); + + // deposit NFT if necessary + // reverts if sender is not approved or original owner + address _originalOwner = _depositNft(_role.tokenAddress, _role.tokenId); + + // role must be expired or revocable + RoleData storage _roleData = roles[_role.tokenAddress][_role.tokenId][_role.roleId]; + require( + _roleData.revocable || _roleData.expirationDate < block.timestamp, + 'NftRolesRegistryVault: role must be expired or revocable' + ); + + roles[_role.tokenAddress][_role.tokenId][_role.roleId] = RoleData( + _role.recipient, + _role.expirationDate, + _role.revocable, + _role.data + ); + + emit RoleGranted( + _role.tokenAddress, + _role.tokenId, + _role.roleId, + _originalOwner, + _role.recipient, + _role.expirationDate, + _role.revocable, + _role.data + ); + } + + function revokeRole(address _tokenAddress, uint256 _tokenId, bytes32 _roleId) external override { + address _recipient = roles[_tokenAddress][_tokenId][_roleId].recipient; + address _caller = _getApprovedCaller(_tokenAddress, _tokenId, _recipient); + + // if caller is recipient, the role can be revoked regardless of its state + if (_caller != _recipient) { + // if caller is owner, the role can only be revoked if revocable or expired + require( + roles[_tokenAddress][_tokenId][_roleId].revocable || + roles[_tokenAddress][_tokenId][_roleId].expirationDate < block.timestamp, + 'NftRolesRegistryVault: role is not revocable nor expired' + ); + } + + delete roles[_tokenAddress][_tokenId][_roleId]; + emit RoleRevoked(_tokenAddress, _tokenId, _roleId); + } + + function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _approved) external override { + tokenApprovals[msg.sender][_tokenAddress][_operator] = _approved; + emit RoleApprovalForAll(_tokenAddress, _operator, _approved); + } + + /** ERC-7432 View Functions **/ + + function recipientOf( + address _tokenAddress, + uint256 _tokenId, + bytes32 _roleId + ) external view returns (address recipient_) { + if ( + _isTokenDeposited(_tokenAddress, _tokenId) && + roles[_tokenAddress][_tokenId][_roleId].expirationDate > block.timestamp + ) { + return roles[_tokenAddress][_tokenId][_roleId].recipient; + } + return address(0); + } + + function roleData( + address _tokenAddress, + uint256 _tokenId, + bytes32 _roleId + ) external view returns (bytes memory data_) { + if (!_isTokenDeposited(_tokenAddress, _tokenId)) { + return ''; + } + return roles[_tokenAddress][_tokenId][_roleId].data; + } + + function roleExpirationDate( + address _tokenAddress, + uint256 _tokenId, + bytes32 _roleId + ) external view returns (uint64 expirationDate_) { + if (!_isTokenDeposited(_tokenAddress, _tokenId)) { + return 0; + } + return roles[_tokenAddress][_tokenId][_roleId].expirationDate; + } + + function isRoleRevocable( + address _tokenAddress, + uint256 _tokenId, + bytes32 _roleId + ) external view returns (bool revocable_) { + if (!_isTokenDeposited(_tokenAddress, _tokenId)) { + return false; + } + return roles[_tokenAddress][_tokenId][_roleId].revocable; + } + + function isRoleApprovedForAll(address _tokenAddress, address _owner, address _operator) public view returns (bool) { + return tokenApprovals[_owner][_tokenAddress][_operator]; + } + + /** ERC-7432 Vault Extension Functions **/ + + function withdraw(address _tokenAddress, uint256 _tokenId) external override { + address originalOwner = originalOwners[_tokenAddress][_tokenId]; + + require(_isWithdrawable(_tokenAddress, _tokenId), 'NftRolesRegistryVault: NFT is not withdrawable'); + + require( + originalOwner == msg.sender || isRoleApprovedForAll(_tokenAddress, originalOwner, msg.sender), + 'NftRolesRegistryVault: sender must be owner or approved' + ); + + delete originalOwners[_tokenAddress][_tokenId]; + IERC721(_tokenAddress).transferFrom(address(this), originalOwner, _tokenId); + emit Withdraw(originalOwner, _tokenAddress, _tokenId); + } + + /** ERC-165 Functions **/ + + function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) { + return interfaceId == type(IERC7432).interfaceId || interfaceId == type(IERC7432VaultExtension).interfaceId; + } + + /** Internal Functions **/ + + /// @notice Updates originalOwner, validates the sender and deposits NFT (if not deposited yet). + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @return originalOwner_ The original owner of the NFT. + function _depositNft(address _tokenAddress, uint256 _tokenId) internal returns (address originalOwner_) { + address _currentOwner = IERC721(_tokenAddress).ownerOf(_tokenId); + + if (_currentOwner == address(this)) { + // if the NFT is already on the contract, check if sender is approved or original owner + originalOwner_ = originalOwners[_tokenAddress][_tokenId]; + require( + originalOwner_ == msg.sender || isRoleApprovedForAll(_tokenAddress, originalOwner_, msg.sender), + 'NftRolesRegistryVault: sender must be owner or approved' + ); + } else { + // if NFT is not in the contract, deposit it and store the original owner + require( + _currentOwner == msg.sender || isRoleApprovedForAll(_tokenAddress, _currentOwner, msg.sender), + 'NftRolesRegistryVault: sender must be owner or approved' + ); + IERC721(_tokenAddress).transferFrom(_currentOwner, address(this), _tokenId); + originalOwners[_tokenAddress][_tokenId] = _currentOwner; + originalOwner_ = _currentOwner; + } + } + + /// @notice Returns the account approved to call the revokeRole function. Reverts otherwise. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _recipient The user that received the role. + /// @return caller_ The approved account. + function _getApprovedCaller( + address _tokenAddress, + uint256 _tokenId, + address _recipient + ) internal view returns (address caller_) { + if (msg.sender == _recipient || isRoleApprovedForAll(_tokenAddress, _recipient, msg.sender)) { + return _recipient; + } + address originalOwner = originalOwners[_tokenAddress][_tokenId]; + if (msg.sender == originalOwner || isRoleApprovedForAll(_tokenAddress, originalOwner, msg.sender)) { + return originalOwner; + } + revert('NftRolesRegistryVault: role does not exist or sender is not approved'); + } + + /// @notice Check if an NFT is withdrawable. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @return True if the NFT is withdrawable. + function _isWithdrawable(address _tokenAddress, uint256 _tokenId) internal view returns (bool) { + // todo needs to implement a way to track expiration dates to make sure NFTs are withdrawable + // mocked result + return _isTokenDeposited(_tokenAddress, _tokenId); + } + + /// @notice Checks if the NFT is deposited on this contract. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @return deposited_ Whether the NFT is deposited or not. + function _isTokenDeposited(address _tokenAddress, uint256 _tokenId) internal view returns (bool) { + return originalOwners[_tokenAddress][_tokenId] != address(0); + } +} diff --git a/contracts/RolesRegistry.sol b/contracts/RolesRegistry.sol deleted file mode 100644 index 3d13ab5..0000000 --- a/contracts/RolesRegistry.sol +++ /dev/null @@ -1,213 +0,0 @@ -// 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'; -import { ERC165Checker } from '@openzeppelin/contracts/utils/introspection/ERC165Checker.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 latestGrantees; - - // grantor => tokenAddress => operator => isApproved - mapping(address => mapping(address => mapping(address => bool))) public tokenApprovals; - - modifier validExpirationDate(uint64 _expirationDate) { - require(_expirationDate > block.timestamp, 'RolesRegistry: expiration date must be in the future'); - _; - } - - modifier onlyOwnerOrApproved( - address _tokenAddress, - uint256 _tokenId, - address _account - ) { - address _tokenOwner = IERC721(_tokenAddress).ownerOf(_tokenId); - require( - msg.sender == _tokenOwner || - (isRoleApprovedForAll(_tokenAddress, _account, msg.sender) && _account == _tokenOwner), - 'RolesRegistry: sender must be token owner or approved' - ); - _; - } - - modifier isTokenOwner( - address _tokenAddress, - uint256 _tokenId, - address _account - ) { - require(_account == IERC721(_tokenAddress).ownerOf(_tokenId), 'RolesRegistry: account must be token owner'); - _; - } - - function grantRoleFrom( - RoleAssignment calldata _roleAssignment - ) - external - override - onlyOwnerOrApproved(_roleAssignment.tokenAddress, _roleAssignment.tokenId, _roleAssignment.grantor) - { - _grantRole(_roleAssignment, false); - } - - function grantRevocableRoleFrom( - RoleAssignment calldata _roleAssignment - ) - external - override - onlyOwnerOrApproved(_roleAssignment.tokenAddress, _roleAssignment.tokenId, _roleAssignment.grantor) - { - _grantRole(_roleAssignment, true); - } - - function _grantRole( - RoleAssignment calldata _roleAssignment, - bool _revocable - ) internal validExpirationDate(_roleAssignment.expirationDate) { - address _lastGrantee = latestGrantees[_roleAssignment.tokenAddress][_roleAssignment.tokenId][ - _roleAssignment.role - ]; - RoleData memory _roleData = roleAssignments[_lastGrantee][_roleAssignment.tokenAddress][ - _roleAssignment.tokenId - ][_roleAssignment.role]; - - bool _hasActiveAssignment = _roleData.expirationDate > block.timestamp; - - if (_hasActiveAssignment) { - // only unique roles can be revocable - require(_roleData.revocable, 'RolesRegistry: role is not revocable'); - } - - roleAssignments[_roleAssignment.grantee][_roleAssignment.tokenAddress][_roleAssignment.tokenId][ - _roleAssignment.role - ] = RoleData(_roleAssignment.expirationDate, _revocable, _roleAssignment.data); - latestGrantees[_roleAssignment.tokenAddress][_roleAssignment.tokenId][_roleAssignment.role] = _roleAssignment - .grantee; - emit RoleGranted( - _roleAssignment.role, - _roleAssignment.tokenAddress, - _roleAssignment.tokenId, - _roleAssignment.grantor, - _roleAssignment.grantee, - _roleAssignment.expirationDate, - _revocable, - _roleAssignment.data - ); - } - - function revokeRoleFrom( - bytes32 _role, - address _tokenAddress, - uint256 _tokenId, - address _revoker, - address _grantee - ) external override isTokenOwner(_tokenAddress, _tokenId, _revoker) { - address _caller = msg.sender == _revoker || msg.sender == _grantee - ? msg.sender - : _getApprovedCaller(_tokenAddress, _revoker, _grantee); - _revokeRole(_role, _tokenAddress, _tokenId, _revoker, _grantee, _caller); - } - - function _getApprovedCaller( - address _tokenAddress, - address _revoker, - address _grantee - ) internal view returns (address) { - if (isRoleApprovedForAll(_tokenAddress, _grantee, msg.sender)) { - return _grantee; - } else if (isRoleApprovedForAll(_tokenAddress, _revoker, msg.sender)) { - return _revoker; - } else { - revert('RolesRegistry: sender must be approved'); - } - } - - function _revokeRole( - bytes32 _role, - address _tokenAddress, - uint256 _tokenId, - address _revoker, - address _grantee, - address _caller - ) internal { - require( - _caller == _grantee || roleAssignments[_grantee][_tokenAddress][_tokenId][_role].revocable, - 'RolesRegistry: Role is not revocable or caller is not the grantee' - ); - delete roleAssignments[_grantee][_tokenAddress][_tokenId][_role]; - delete latestGrantees[_tokenAddress][_tokenId][_role]; - emit RoleRevoked(_role, _tokenAddress, _tokenId, _revoker, _grantee); - } - - function hasNonUniqueRole( - bytes32 _role, - address _tokenAddress, - uint256 _tokenId, - address _grantor, // not used, but needed for compatibility with ERC7432 - address _grantee - ) external view returns (bool) { - return roleAssignments[_grantee][_tokenAddress][_tokenId][_role].expirationDate > block.timestamp; - } - - function hasRole( - bytes32 _role, - address _tokenAddress, - uint256 _tokenId, - address _grantor, // not used, but needed for compatibility with ERC7432 - address _grantee - ) external view returns (bool) { - return - latestGrantees[_tokenAddress][_tokenId][_role] == _grantee && - roleAssignments[_grantee][_tokenAddress][_tokenId][_role].expirationDate > block.timestamp; - } - - function roleData( - bytes32 _role, - address _tokenAddress, - uint256 _tokenId, - address _grantor, // not used, but needed for compatibility with ERC7432 - address _grantee - ) external view returns (RoleData memory) { - return roleAssignments[_grantee][_tokenAddress][_tokenId][_role]; - } - - function roleExpirationDate( - bytes32 _role, - address _tokenAddress, - uint256 _tokenId, - address _grantor, // not used, but needed for compatibility with ERC7432 - address _grantee - ) external view returns (uint64 expirationDate_) { - return roleAssignments[_grantee][_tokenAddress][_tokenId][_role].expirationDate; - } - - function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) { - return interfaceId == type(IERC7432).interfaceId; - } - - function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _isApproved) external override { - tokenApprovals[msg.sender][_tokenAddress][_operator] = _isApproved; - emit RoleApprovalForAll(_tokenAddress, _operator, _isApproved); - } - - function isRoleApprovedForAll( - address _tokenAddress, - address _grantor, - address _operator - ) public view override returns (bool) { - return tokenApprovals[_grantor][_tokenAddress][_operator]; - } - - function lastGrantee( - bytes32 _role, - address _tokenAddress, - uint256 _tokenId, - address _grantor // not used, but needed for compatibility with ERC7432 - ) public view override returns (address) { - return latestGrantees[_tokenAddress][_tokenId][_role]; - } -} diff --git a/contracts/interfaces/IERC7432.sol b/contracts/interfaces/IERC7432.sol index 5e0ac04..d0f103d 100644 --- a/contracts/interfaces/IERC7432.sol +++ b/contracts/interfaces/IERC7432.sol @@ -6,91 +6,65 @@ 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 0x04984ac8. +/// Note: the ERC-165 identifier for this interface is 0xfecc9ed3. interface IERC7432 is IERC165 { - struct RoleData { - uint64 expirationDate; - bool revocable; - bytes data; - } - - struct RoleAssignment { - bytes32 role; + struct Role { + bytes32 roleId; address tokenAddress; uint256 tokenId; - address grantor; - address grantee; + address recipient; uint64 expirationDate; + bool revocable; bytes data; } /** Events **/ /// @notice Emitted when a role is granted. - /// @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 receiving the role. + /// @param _roleId The role identifier. + /// @param _owner The user assigning the role. + /// @param _recipient The user receiving the role. /// @param _expirationDate The expiration date of the role. /// @param _revocable Whether the role is revocable or not. /// @param _data Any additional data about the role. event RoleGranted( - bytes32 indexed _role, address indexed _tokenAddress, uint256 indexed _tokenId, - address _grantor, - address _grantee, + bytes32 indexed _roleId, + address _owner, + address _recipient, uint64 _expirationDate, bool _revocable, bytes _data ); /// @notice Emitted when a role is revoked. - /// @param _role The role identifier. /// @param _tokenAddress The token address. /// @param _tokenId The token identifier. - /// @param _revoker The user revoking the role. - /// @param _grantee The user that receives the role revocation. - event RoleRevoked( - bytes32 indexed _role, - address indexed _tokenAddress, - uint256 indexed _tokenId, - address _revoker, - address _grantee - ); + /// @param _roleId The role identifier. + event RoleRevoked(address indexed _tokenAddress, uint256 indexed _tokenId, bytes32 indexed _roleId); - /// @notice Emitted when a user is approved to manage any role on behalf of another user. + /// @notice Emitted when a user is approved to manage roles on behalf of another user. /// @param _tokenAddress The token address. /// @param _operator The user approved to grant and revoke roles. /// @param _isApproved The approval status. - event RoleApprovalForAll(address indexed _tokenAddress, address indexed _operator, bool _isApproved); + event RoleApprovalForAll(address indexed _tokenAddress, address indexed _operator, bool indexed _isApproved); /** External Functions **/ - /// @notice Grants a role on behalf of a user. - /// @param _roleAssignment The role assignment data. - function grantRoleFrom(RoleAssignment calldata _roleAssignment) external; + /// @notice Grants a role to a user. + /// @param _role The role attributes. + function grantRole(Role calldata _role) external; - /// @notice Grants a role on behalf of a user. - /// @param _roleAssignment The role assignment data. - function grantRevocableRoleFrom(RoleAssignment calldata _roleAssignment) external; - - /// @notice Revokes a role on behalf of a user. - /// @param _role The role identifier. + /// @notice Revokes a role from a user. /// @param _tokenAddress The token address. /// @param _tokenId The token identifier. - /// @param _revoker The user revoking the role. - /// @param _grantee The user that receives the role revocation. - function revokeRoleFrom( - bytes32 _role, - address _tokenAddress, - uint256 _tokenId, - address _revoker, - address _grantee - ) external; + /// @param _roleId The role identifier. + function revokeRole(address _tokenAddress, uint256 _tokenId, bytes32 _roleId) external; - /// @notice Approves operator to grant and revoke any roles on behalf of another user. + /// @notice Approves operator to grant and revoke roles on behalf of another user. /// @param _tokenAddress The token address. /// @param _operator The user approved to grant and revoke roles. /// @param _approved The approval status. @@ -98,81 +72,58 @@ interface IERC7432 is IERC165 { /** View Functions **/ - /// @notice Checks if a user has a role. - /// @param _role The role identifier. + /// @notice Retrieves the recipient of an NFT role. /// @param _tokenAddress The token address. /// @param _tokenId The token identifier. - /// @param _grantor The user that assigned the role. - /// @param _grantee The user that received the role. - function hasNonUniqueRole( - bytes32 _role, + /// @param _roleId The role identifier. + /// @return recipient_ The user that received the role. + function recipientOf( address _tokenAddress, uint256 _tokenId, - address _grantor, - address _grantee - ) external view returns (bool); + bytes32 _roleId + ) external view returns (address recipient_); - /// @notice Checks if a user has a unique role. - /// @param _role The role identifier. + /// @notice Retrieves the custom data of a role assignment. /// @param _tokenAddress The token address. /// @param _tokenId The token identifier. - /// @param _grantor The user that assigned the role. - /// @param _grantee The user that received the role. - function hasRole( - bytes32 _role, + /// @param _roleId The role identifier. + /// @return data_ The custom data of the role. + function roleData( address _tokenAddress, uint256 _tokenId, - address _grantor, - address _grantee - ) external view returns (bool); + bytes32 _roleId + ) external view returns (bytes memory data_); - /// @notice Returns the custom data of a role assignment. - /// @param _role The role identifier. + /// @notice Retrieves the expiration date of a role assignment. /// @param _tokenAddress The token address. /// @param _tokenId The token identifier. - /// @param _grantor The user that assigned the role. - /// @param _grantee The user that received the role. - function roleData( - bytes32 _role, + /// @param _roleId The role identifier. + /// @return expirationDate_ The expiration date of the role. + function roleExpirationDate( address _tokenAddress, uint256 _tokenId, - address _grantor, - address _grantee - ) external view returns (RoleData memory data_); + bytes32 _roleId + ) external view returns (uint64 expirationDate_); - /// @notice Returns the expiration date of a role assignment. - /// @param _role The role identifier. + /// @notice Verifies if the role is revocable. /// @param _tokenAddress The token address. /// @param _tokenId The token identifier. - /// @param _grantor The user that assigned the role. - /// @param _grantee The user that received the role. - function roleExpirationDate( - bytes32 _role, + /// @param _roleId The role identifier. + /// @return revocable_ Whether the role is revocable. + function isRoleRevocable( address _tokenAddress, uint256 _tokenId, - address _grantor, - address _grantee - ) external view returns (uint64 expirationDate_); + bytes32 _roleId + ) external view returns (bool revocable_); - /// @notice Checks if the grantor approved the operator for all NFTs. + /// @notice Verifies if the owner approved the operator. /// @param _tokenAddress The token address. - /// @param _grantor The user that approved the operator. + /// @param _owner The user that approved the operator. /// @param _operator The user that can grant and revoke roles. + /// @return Whether the operator is approved. function isRoleApprovedForAll( address _tokenAddress, - address _grantor, + address _owner, address _operator ) external view returns (bool); - - /// @notice Returns the last grantee of a role. - /// @param _role The role. - /// @param _tokenAddress The token address. - /// @param _tokenId The token ID. - /// @param _grantor The user that granted the role. - function lastGrantee( - bytes32 _role, - address _tokenAddress, - uint256 _tokenId, - address _grantor - ) external view returns (address); } diff --git a/contracts/interfaces/IERC7432VaultExtension.sol b/contracts/interfaces/IERC7432VaultExtension.sol new file mode 100644 index 0000000..0f35335 --- /dev/null +++ b/contracts/interfaces/IERC7432VaultExtension.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +/// @title ERC-7432 Vault Extension +/// @dev See https://eips.ethereum.org/EIPS/eip-7432 +/// Note: the ERC-165 identifier for this interface is 0xf3fef3a3. +interface IERC7432VaultExtension { + /** Events **/ + + /// @notice Emitted when an NFT is withdrawn. + /// @param _owner The original owner of the NFT. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + event Withdraw(address indexed _owner, address indexed _tokenAddress, uint256 indexed _tokenId); + + /** External Functions **/ + + /// @notice Withdraw NFT back to original owner. + /// @dev Reverts if sender is not approved or the original owner. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + function withdraw(address _tokenAddress, uint256 _tokenId) external; +} diff --git a/test/ERC7432/NftRolesRegistryVault.ts b/test/ERC7432/NftRolesRegistryVault.ts new file mode 100644 index 0000000..d081187 --- /dev/null +++ b/test/ERC7432/NftRolesRegistryVault.ts @@ -0,0 +1,370 @@ +import { ethers } from 'hardhat' +import { Contract } from 'ethers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { beforeEach } from 'mocha' +import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers' +import { Role } from '../types' +import { buildRole, getExpiredDate } from './mockData' +import { expect } from 'chai' +import { IERC7432__factory, IERC7432VaultExtension__factory } from '../../typechain-types' +import { generateErc165InterfaceId, ONE_DAY } from '../helpers' + +const { AddressZero, HashZero } = ethers.constants + +describe('NftRolesRegistryVault', () => { + let NftRolesRegistryVault: Contract + let MockErc721Token: Contract + let owner: SignerWithAddress + let recipient: SignerWithAddress + let anotherUser: SignerWithAddress + let role: Role + + async function deployContracts() { + const SftRolesRegistryFactory = await ethers.getContractFactory('NftRolesRegistryVault') + NftRolesRegistryVault = await SftRolesRegistryFactory.deploy() + const MockErc721TokenFactory = await ethers.getContractFactory('MockERC721') + MockErc721Token = await MockErc721TokenFactory.deploy() + const signers = await ethers.getSigners() + owner = signers[0] + recipient = signers[1] + anotherUser = signers[2] + } + + async function depositNftAndGrantRole({ recipient = AddressZero }) { + await MockErc721Token.connect(owner).approve(NftRolesRegistryVault.address, role.tokenId) + await expect(NftRolesRegistryVault.connect(owner).grantRole({ ...role, recipient })) + .to.emit(NftRolesRegistryVault, 'RoleGranted') + .withArgs( + role.tokenAddress, + role.tokenId, + role.roleId, + owner.address, + recipient, + role.expirationDate, + role.revocable, + role.data, + ) + .to.emit(MockErc721Token, 'Transfer') + .withArgs(owner.address, NftRolesRegistryVault.address, role.tokenId) + } + + beforeEach(async () => { + await loadFixture(deployContracts) + role = await buildRole({ + tokenAddress: MockErc721Token.address, + }) + MockErc721Token.mint(owner.address, role.tokenId) + }) + + describe('grantRole', () => { + it('should revert when expiration date is in the past', async () => { + const expirationDate = await getExpiredDate() + const role = await buildRole({ expirationDate }) + await expect(NftRolesRegistryVault.connect(owner).grantRole(role)).to.be.revertedWith( + 'NftRolesRegistryVault: expiration date must be in the future', + ) + }) + + it('should revert when tokenAddress is not an ERC-721', async () => { + const role = await buildRole({}) + await expect(NftRolesRegistryVault.connect(owner).grantRole(role)).to.be.reverted + }) + + describe('when NFT is not deposited', () => { + it('should revert when sender is not approved or owner', async () => { + await expect(NftRolesRegistryVault.connect(recipient).grantRole(role)).to.be.revertedWith( + 'NftRolesRegistryVault: sender must be owner or approved', + ) + }) + + it('should revert when contract is not approved to transfer NFT', async () => { + await expect(NftRolesRegistryVault.connect(owner).grantRole(role)).to.be.revertedWith( + 'ERC721: caller is not token owner or approved', + ) + }) + + it('should revert when sender is role approved, but contract is not approved to transfer NFT', async () => { + await NftRolesRegistryVault.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await expect(NftRolesRegistryVault.connect(anotherUser).grantRole(role)).to.be.revertedWith( + 'ERC721: caller is not token owner or approved', + ) + }) + + it('should grant role when sender is NFT owner', async () => { + await depositNftAndGrantRole({}) + }) + + it('should grant role when sender is approved', async () => { + await MockErc721Token.connect(owner).approve(NftRolesRegistryVault.address, role.tokenId) + await NftRolesRegistryVault.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await expect(NftRolesRegistryVault.connect(anotherUser).grantRole(role)) + .to.emit(NftRolesRegistryVault, 'RoleGranted') + .withArgs( + role.tokenAddress, + role.tokenId, + role.roleId, + owner.address, + role.recipient, + role.expirationDate, + role.revocable, + role.data, + ) + .to.emit(MockErc721Token, 'Transfer') + .withArgs(owner.address, NftRolesRegistryVault.address, role.tokenId) + }) + }) + + describe('when NFT is deposited', () => { + beforeEach(async () => { + await depositNftAndGrantRole({}) + }) + + it('should revert when sender is not approved or original owner', async () => { + await expect(NftRolesRegistryVault.connect(anotherUser).grantRole(role)).to.be.revertedWith( + 'NftRolesRegistryVault: sender must be owner or approved', + ) + }) + + it('should grant role when sender is original owner', async () => { + await expect(NftRolesRegistryVault.connect(owner).grantRole(role)) + .to.emit(NftRolesRegistryVault, 'RoleGranted') + .withArgs( + role.tokenAddress, + role.tokenId, + role.roleId, + owner.address, + role.recipient, + role.expirationDate, + role.revocable, + role.data, + ) + .to.not.emit(MockErc721Token, 'Transfer') + }) + + it('should grant role when sender is approved', async () => { + await NftRolesRegistryVault.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await expect(NftRolesRegistryVault.connect(anotherUser).grantRole(role)) + .to.emit(NftRolesRegistryVault, 'RoleGranted') + .withArgs( + role.tokenAddress, + role.tokenId, + role.roleId, + owner.address, + role.recipient, + role.expirationDate, + role.revocable, + role.data, + ) + .to.not.emit(MockErc721Token, 'Transfer') + }) + + it('should revert when there is a non-expired and non-revocable role', async () => { + await NftRolesRegistryVault.connect(owner).grantRole({ ...role, revocable: false }) + await expect(NftRolesRegistryVault.connect(owner).grantRole(role)).to.be.revertedWith( + 'NftRolesRegistryVault: role must be expired or revocable', + ) + }) + }) + }) + + describe('revokeRole', () => { + beforeEach(async () => { + await depositNftAndGrantRole({ recipient: recipient.address }) + }) + + it('should revert when sender is not owner, recipient or approved', async () => { + await expect( + NftRolesRegistryVault.connect(anotherUser).revokeRole(role.tokenAddress, role.tokenId, role.roleId), + ).to.be.revertedWith('NftRolesRegistryVault: role does not exist or sender is not approved') + }) + + it('should revert when sender is owner but role is not revocable nor expired', async () => { + await expect(NftRolesRegistryVault.connect(owner).grantRole({ ...role, revocable: false })) + + await expect( + NftRolesRegistryVault.connect(owner).revokeRole(role.tokenAddress, role.tokenId, role.roleId), + ).to.be.revertedWith('NftRolesRegistryVault: role is not revocable nor expired') + }) + + it('should revoke role when sender is recipient', async () => { + await expect(NftRolesRegistryVault.connect(recipient).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(NftRolesRegistryVault, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + }) + + it('should revoke role when sender is approved by recipient', async () => { + await NftRolesRegistryVault.connect(recipient).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await expect(NftRolesRegistryVault.connect(anotherUser).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(NftRolesRegistryVault, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + }) + + it('should revoke role when sender is owner (and role is revocable)', async () => { + await expect(NftRolesRegistryVault.connect(owner).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(NftRolesRegistryVault, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + }) + + it('should revoke role when sender is owner, and role is not revocable but is expired', async () => { + await expect(NftRolesRegistryVault.connect(owner).grantRole({ ...role, revocable: false })) + .to.emit(NftRolesRegistryVault, 'RoleGranted') + .withArgs( + role.tokenAddress, + role.tokenId, + role.roleId, + owner.address, + role.recipient, + role.expirationDate, + false, + role.data, + ) + .to.not.emit(MockErc721Token, 'Transfer') + await time.increase(ONE_DAY) + await expect(NftRolesRegistryVault.connect(owner).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(NftRolesRegistryVault, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + }) + + it('should revoke role when sender is approved by owner (and role is revocable)', async () => { + await NftRolesRegistryVault.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await expect(NftRolesRegistryVault.connect(anotherUser).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(NftRolesRegistryVault, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + }) + + it('should revoke role when sender is approved both by owner and recipient, and role not revocable', async () => { + await expect( + NftRolesRegistryVault.connect(owner).grantRole({ + ...role, + recipient: recipient.address, + revocable: false, + }), + ) + await NftRolesRegistryVault.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await NftRolesRegistryVault.connect(recipient).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await expect(NftRolesRegistryVault.connect(anotherUser).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(NftRolesRegistryVault, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + }) + + it('should not delete original owner when revoking role', async () => { + await expect(NftRolesRegistryVault.connect(owner).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(NftRolesRegistryVault, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + + expect(await NftRolesRegistryVault.originalOwners(role.tokenAddress, role.tokenId)).to.be.equal(owner.address) + }) + }) + + describe('withdraw', () => { + beforeEach(async () => { + await depositNftAndGrantRole({ recipient: recipient.address }) + }) + + it('should revert if token is not deposited', async () => { + await expect( + NftRolesRegistryVault.connect(owner).withdraw(role.tokenAddress, role.tokenId + 1), + ).to.be.revertedWith('NftRolesRegistryVault: NFT is not withdrawable') + }) + + it('should revert if sender is not original owner or approved', async () => { + await expect( + NftRolesRegistryVault.connect(anotherUser).withdraw(role.tokenAddress, role.tokenId), + ).to.be.revertedWith('NftRolesRegistryVault: sender must be owner or approved') + }) + + it('should withdraw if sender is owner and NFT is withdrawable', async () => { + await expect(NftRolesRegistryVault.connect(owner).withdraw(role.tokenAddress, role.tokenId)) + .to.emit(NftRolesRegistryVault, 'Withdraw') + .withArgs(owner.address, role.tokenAddress, role.tokenId) + .to.emit(MockErc721Token, 'Transfer') + .withArgs(NftRolesRegistryVault.address, owner.address, role.tokenId) + }) + + it('should withdraw if sender is approved and NFT is withdrawable', async () => { + await NftRolesRegistryVault.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await expect(NftRolesRegistryVault.connect(anotherUser).withdraw(role.tokenAddress, role.tokenId)) + .to.emit(NftRolesRegistryVault, 'Withdraw') + .withArgs(owner.address, role.tokenAddress, role.tokenId) + .to.emit(MockErc721Token, 'Transfer') + .withArgs(NftRolesRegistryVault.address, owner.address, role.tokenId) + }) + }) + + describe('view functions', async () => { + describe('when NFT is deposited', async () => { + it('hasRole should return default value', async () => { + expect(await NftRolesRegistryVault.recipientOf(role.tokenAddress, role.tokenId, role.roleId)).to.be.equal( + AddressZero, + ) + }) + + it('roleData should return default value', async () => { + expect(await NftRolesRegistryVault.roleData(role.tokenAddress, role.tokenId, role.roleId)).to.be.equal('0x') + }) + + it('roleExpirationDate should return default value', async () => { + expect( + await NftRolesRegistryVault.roleExpirationDate(role.tokenAddress, role.tokenId, role.roleId), + ).to.be.equal(0) + }) + + it('isRoleRevocable should return default value', async () => { + expect(await NftRolesRegistryVault.isRoleRevocable(role.tokenAddress, role.tokenId, role.roleId)).to.be.false + }) + }) + + describe('when NFT is deposited', async () => { + beforeEach(async () => { + await depositNftAndGrantRole({ recipient: recipient.address }) + }) + + it('hasRole should return value from mapping', async () => { + expect(await NftRolesRegistryVault.recipientOf(role.tokenAddress, role.tokenId, role.roleId)).to.be.equal( + recipient.address, + ) + }) + + it('roleData should return the custom data of the role', async () => { + expect(await NftRolesRegistryVault.roleData(role.tokenAddress, role.tokenId, role.roleId)).to.be.equal(HashZero) + }) + + it('roleExpirationDate should the expiration date of the role', async () => { + expect( + await NftRolesRegistryVault.roleExpirationDate(role.tokenAddress, role.tokenId, role.roleId), + ).to.be.equal(role.expirationDate) + }) + + it('isRoleRevocable should whether the role is revocable', async () => { + expect(await NftRolesRegistryVault.isRoleRevocable(role.tokenAddress, role.tokenId, role.roleId)).to.be.true + }) + }) + }) + + describe('isRoleApprovedForAll', async () => { + it('should return false when not approved', async () => { + expect(await NftRolesRegistryVault.isRoleApprovedForAll(role.tokenAddress, owner.address, anotherUser.address)).to + .be.false + }) + + it('should return true when approved', async () => { + await NftRolesRegistryVault.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + expect(await NftRolesRegistryVault.isRoleApprovedForAll(role.tokenAddress, owner.address, anotherUser.address)).to + .be.true + }) + }) + + describe('ERC-165', async () => { + it('should return true when IERC7432 identifier is provided', async () => { + const iface = IERC7432__factory.createInterface() + const ifaceId = generateErc165InterfaceId(iface) + expect(await NftRolesRegistryVault.supportsInterface(ifaceId)).to.be.true + }) + + it('should return true when IERC7432VaultExtension identifier is provided', async () => { + const iface = IERC7432VaultExtension__factory.createInterface() + const ifaceId = generateErc165InterfaceId(iface) + expect(await NftRolesRegistryVault.supportsInterface(ifaceId)).to.be.true + }) + }) +}) diff --git a/test/ERC7432/mockData.ts b/test/ERC7432/mockData.ts new file mode 100644 index 0000000..68e9c7a --- /dev/null +++ b/test/ERC7432/mockData.ts @@ -0,0 +1,32 @@ +import { time } from '@nomicfoundation/hardhat-network-helpers' +import { ethers } from 'ethers' +import { ONE_DAY, generateRandomInt, generateRoleId, ROLE } from '../helpers' +import { Role } from '../types' + +const { HashZero, AddressZero } = ethers.constants + +export async function buildRole({ + roleId = ROLE, + tokenAddress = AddressZero, + tokenId = generateRandomInt(), + recipient = AddressZero, + expirationDate = 0, + revocable = true, + data = HashZero, +}): Promise { + return { + roleId: generateRoleId(roleId), + tokenAddress, + tokenId, + recipient, + expirationDate: expirationDate ? expirationDate : (await time.latest()) + ONE_DAY, + revocable, + data, + } +} + +export async function getExpiredDate(): Promise { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const block = await require('hardhat').ethers.provider.getBlock('latest') + return block.timestamp - 1 +} diff --git a/test/RolesRegistry.spec.ts b/test/RolesRegistry.spec.ts deleted file mode 100644 index e5e7744..0000000 --- a/test/RolesRegistry.spec.ts +++ /dev/null @@ -1,781 +0,0 @@ -import hre, { ethers } from 'hardhat' -import { Contract } from 'ethers' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { ERC7432InterfaceId } from './contants' -import nock from 'nock' -import { defaultAbiCoder, solidityKeccak256 } from 'ethers/lib/utils' -import { NftMetadata, RoleAssignment } from './types' - -const { HashZero } = ethers.constants -const ONE_DAY = 60 * 60 * 24 - -describe('RolesRegistry', () => { - let RolesRegistry: Contract - let mockERC721: Contract - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let deployer: SignerWithAddress - let grantor: SignerWithAddress - let userOne: SignerWithAddress - let userTwo: SignerWithAddress - let operator: SignerWithAddress - - const PROPERTY_MANAGER = solidityKeccak256(['string'], ['PROPERTY_MANAGER']) - const PROPERTY_TENANT = solidityKeccak256(['string'], ['PROPERTY_TENANT']) - - const tokenId = 1 - const revocable = true - - before(async function () { - // prettier-ignore - [deployer, grantor, userOne, userTwo, operator] = await ethers.getSigners() - - const metadata: NftMetadata = { - name: 'Nft name', - description: 'Nft description', - roles: [ - { - name: 'PROPERTY_MANAGER', - description: 'Property Manager', - isUniqueRole: false, - inputs: [ - { - name: 'profitSplit', - type: 'tuple[]', - components: [ - { - name: 'eventId', - type: 'uint256', - }, - { - name: 'split', - type: 'uint256[]', - }, - ], - }, - ], - }, - { - name: 'PROPERTY_TENANT', - description: 'Property Tenant', - isUniqueRole: true, - inputs: [ - { - name: 'rentalCost', - type: 'uint256', - }, - ], - }, - ], - } - - nock('https://example.com').persist().get(`/${tokenId}`).reply(200, metadata) - }) - - beforeEach(async () => { - const RolesRegistryFactory = await ethers.getContractFactory('RolesRegistry') - RolesRegistry = await RolesRegistryFactory.deploy() - - const MockERC721Factory = await ethers.getContractFactory('MockERC721') - mockERC721 = await MockERC721Factory.deploy() - await mockERC721.deployed() - - await mockERC721.mint(grantor.address, tokenId) - }) - - describe('Main Functions', async () => { - let expirationDate: number - const data = HashZero - let roleAssignment: RoleAssignment - - beforeEach(async () => { - const blockNumber = await hre.ethers.provider.getBlockNumber() - const block = await hre.ethers.provider.getBlock(blockNumber) - expirationDate = block.timestamp + ONE_DAY - - roleAssignment = { - role: PROPERTY_MANAGER, - tokenAddress: mockERC721.address, - tokenId: tokenId, - grantor: grantor.address, - grantee: userOne.address, - expirationDate: expirationDate, - data: HashZero, - } - }) - - describe('Grant role from', async () => { - it('should grant role from for ERC721', async () => { - await expect(RolesRegistry.connect(grantor).grantRevocableRoleFrom(roleAssignment)) - .to.emit(RolesRegistry, 'RoleGranted') - .withArgs( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - expirationDate, - revocable, - data, - ) - }) - it('should NOT grant role if role is not revocable', async function () { - await expect(RolesRegistry.connect(grantor).grantRoleFrom(roleAssignment)) - await expect(RolesRegistry.connect(grantor).grantRoleFrom(roleAssignment)).to.be.revertedWith( - `RolesRegistry: role is not revocable`, - ) - }) - it('should NOT grant role from if expiration date is in the past', async () => { - const blockNumber = await hre.ethers.provider.getBlockNumber() - const block = await hre.ethers.provider.getBlock(blockNumber) - roleAssignment.expirationDate = block.timestamp - ONE_DAY - - await expect(RolesRegistry.connect(grantor).grantRoleFrom(roleAssignment)).to.be.revertedWith( - 'RolesRegistry: expiration date must be in the future', - ) - }) - it('should NOT grant role from if caller is not the token owner', async () => { - await expect(RolesRegistry.connect(userOne).grantRoleFrom(roleAssignment)).to.be.revertedWith( - `RolesRegistry: sender must be token owner or approved`, - ) - }) - }) - - describe('Revoke role', async () => { - beforeEach(async () => { - await RolesRegistry.connect(grantor).grantRevocableRoleFrom(roleAssignment) - }) - it('should revoke role', async () => { - await expect( - RolesRegistry.connect(grantor).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ) - .to.emit(RolesRegistry, 'RoleRevoked') - .withArgs(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address, userOne.address) - }) - it('should revoke role if caller is the grantee', async () => { - await expect( - RolesRegistry.connect(userOne).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ) - .to.emit(RolesRegistry, 'RoleRevoked') - .withArgs(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address, userOne.address) - }) - it('should revoke role if role is not revocable, but grantor is also the grantee', async () => { - roleAssignment.grantee = grantor.address - await RolesRegistry.connect(grantor).grantRoleFrom(roleAssignment) - await expect( - RolesRegistry.connect(grantor).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - grantor.address, - ), - ) - .to.emit(RolesRegistry, 'RoleRevoked') - .withArgs(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address, grantor.address) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - grantor.address, - ), - ).to.be.equal(false) - }) - it('should NOT revoke role if role is not revocable', async () => { - await RolesRegistry.connect(grantor).grantRoleFrom(roleAssignment) - await expect( - RolesRegistry.connect(grantor).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.revertedWith(`RolesRegistry: Role is not revocable or caller is not the grantee`) - }) - it('should NOT revoke role if caller is not the token owner', async () => { - await expect( - RolesRegistry.connect(userTwo).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.revertedWith(`RolesRegistry: sender must be approved`) - }) - }) - - describe('Has role', async () => { - beforeEach(async () => { - await expect(RolesRegistry.connect(grantor).grantRevocableRoleFrom(roleAssignment)) - .to.emit(RolesRegistry, 'RoleGranted') - .withArgs( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - expirationDate, - revocable, - HashZero, - ) - - roleAssignment.grantee = userTwo.address - await expect(RolesRegistry.connect(grantor).grantRevocableRoleFrom(roleAssignment)) - .to.emit(RolesRegistry, 'RoleGranted') - .withArgs( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userTwo.address, - expirationDate, - revocable, - HashZero, - ) - }) - - describe('Unique Roles', async () => { - it('should return true for the last user granted, and false for the others', async () => { - expect( - await RolesRegistry.hasRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(false) - - expect( - await RolesRegistry.hasRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userTwo.address, - ), - ).to.be.equal(true) - - expect( - await RolesRegistry.lastGrantee(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address), - ).to.be.equal(userTwo.address) - }) - it('should NOT return true for the last user if role is expired', async () => { - await hre.ethers.provider.send('evm_increaseTime', [ONE_DAY + 1]) - await hre.ethers.provider.send('evm_mine', []) - - expect( - await RolesRegistry.hasRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(false) - }) - }) - - describe('Non-Unique Roles', async () => { - it('should return true for all users', async () => { - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(true) - - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userTwo.address, - ), - ).to.be.equal(true) - }) - it("should NOT return true for all users if role is expired'", async () => { - await hre.ethers.provider.send('evm_increaseTime', [ONE_DAY + 1]) - await hre.ethers.provider.send('evm_mine', []) - - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_TENANT, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(false) - - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_TENANT, - mockERC721.address, - tokenId, - grantor.address, - userTwo.address, - ), - ).to.be.equal(false) - }) - }) - }) - - describe('Role Data', async () => { - it('should grant PROPERTY_MANAGER with customData and decode tuple with nftMetadata correctly', async () => { - //Encode profit split data - const profitSplit = [ - { - eventId: 1, - split: [60, 30, 5, 5], - }, - { - eventId: 2, - split: [50, 50], - }, - ] - const customData = defaultAbiCoder.encode(['(uint256 eventId,uint256[] split)[]'], [profitSplit]) - - roleAssignment.data = customData - await expect(RolesRegistry.connect(grantor).grantRevocableRoleFrom(roleAssignment)) - .to.emit(RolesRegistry, 'RoleGranted') - .withArgs( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - expirationDate, - revocable, - customData, - ) - - await RolesRegistry.roleData(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address, userOne.address) - - await RolesRegistry.roleExpirationDate( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ) - - /* expect(returnedExpirationDate).to.equal(expirationDate) - expect(returnedData).to.equal(customData) - - const propertyManagerRole = nftMetadata.roles.find((role: Role) => role.name === 'PROPERTY_MANAGER') - const inputs = propertyManagerRole?.inputs[0].components - const returnDataDecoded = defaultAbiCoder.decode( - [`(${inputs?.map(input => `${input.type} ${input.name}`)})[]`], - returnedData, - ) - returnDataDecoded.map((data: any) => { - data.map((returnedStruct: any, index: number) => { - expect(returnedStruct.eventId).to.deep.equal(profitSplit[index].eventId) - expect(returnedStruct.split).to.deep.equal(profitSplit[index].split) - }) - }) */ - }) - it('should grant PROPERTY_TENANT with customData and decode tuple with nftMetadata correctly', async () => { - // Encode rentalCost data - const rentalCost = ethers.utils.parseEther('1.5') - const customData = defaultAbiCoder.encode(['uint256'], [rentalCost]) - - roleAssignment.role = PROPERTY_TENANT - roleAssignment.data = customData - await RolesRegistry.connect(grantor).grantRevocableRoleFrom(roleAssignment) - - await RolesRegistry.roleData(PROPERTY_TENANT, mockERC721.address, tokenId, grantor.address, userOne.address) - /* - const tenantRole = nftMetadata.roles.find((role: Role) => role.name === 'PROPERTY_TENANT') - const decodedData = defaultAbiCoder.decode([`${tenantRole!.inputs.map(input => input.type)}`], returnedData) - - expect(returnedData).to.equal(customData) - expect(decodedData[0]).to.deep.equal(rentalCost) */ - }) - }) - - describe('ERC165', async function () { - it(`should return true for IERC7432 interface id (${ERC7432InterfaceId})`, async function () { - expect(await RolesRegistry.supportsInterface(ERC7432InterfaceId)).to.be.true - }) - }) - - describe('Approvals', async () => { - describe('Approval for All', async () => { - beforeEach(async () => { - await RolesRegistry.connect(grantor).setRoleApprovalForAll(mockERC721.address, operator.address, true) - }) - describe('Grant revocable role from', async () => { - it('should Grant revocable role from', async () => { - await expect(RolesRegistry.connect(operator).grantRevocableRoleFrom(roleAssignment)) - .to.emit(RolesRegistry, 'RoleGranted') - .withArgs( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - expirationDate, - revocable, - data, - ) - }) - it('should NOT Grant revocable role from if operator is not approved', async () => { - await RolesRegistry.connect(grantor).setRoleApprovalForAll(mockERC721.address, operator.address, false) - - await expect(RolesRegistry.connect(operator).grantRevocableRoleFrom(roleAssignment)).to.be.revertedWith( - 'RolesRegistry: sender must be token owner or approved', - ) - }) - it('should NOT Grant revocable role from if grantor is not the token owner', async () => { - await mockERC721.connect(grantor).transferFrom(grantor.address, userOne.address, tokenId) - await expect(RolesRegistry.connect(operator).grantRevocableRoleFrom(roleAssignment)).to.be.revertedWith( - `RolesRegistry: sender must be token owner or approved`, - ) - }) - }) - - describe('Revoke role from', async () => { - describe('Revocable roles', async () => { - beforeEach(async () => { - await RolesRegistry.connect(operator).grantRevocableRoleFrom(roleAssignment) - }) - it('should revoke role from', async () => { - await expect( - RolesRegistry.connect(operator).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ) - .to.emit(RolesRegistry, 'RoleRevoked') - .withArgs(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address, userOne.address) - }) - it('should revoke role from if operator is only approved by grantee', async () => { - await RolesRegistry.connect(grantor).setRoleApprovalForAll(mockERC721.address, operator.address, false) - await RolesRegistry.connect(userOne).setRoleApprovalForAll(mockERC721.address, operator.address, true) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(true) - await expect( - RolesRegistry.connect(operator).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ) - .to.emit(RolesRegistry, 'RoleRevoked') - .withArgs(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address, userOne.address) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(false) - }) - it('should revoke role from if operator is approved by both grantor and grantee', async () => { - await RolesRegistry.connect(grantor).setRoleApprovalForAll(mockERC721.address, operator.address, true) - await RolesRegistry.connect(userOne).setRoleApprovalForAll(mockERC721.address, operator.address, true) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(true) - await expect( - RolesRegistry.connect(operator).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ) - .to.emit(RolesRegistry, 'RoleRevoked') - .withArgs(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address, userOne.address) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(false) - }) - it('should revoke role from if operator is only approved by grantor', async () => { - await RolesRegistry.connect(grantor).setRoleApprovalForAll(mockERC721.address, operator.address, true) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(true) - await expect( - RolesRegistry.connect(operator).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ) - .to.emit(RolesRegistry, 'RoleRevoked') - .withArgs(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address, userOne.address) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(false) - }) - it('should NOT revoke role from if operator is not approved', async () => { - await RolesRegistry.connect(grantor).setRoleApprovalForAll(mockERC721.address, operator.address, false) - await expect( - RolesRegistry.connect(operator).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.revertedWith('RolesRegistry: sender must be approved') - }) - it('should NOT revoke role from if revoker is not the token owner', async () => { - await mockERC721.connect(grantor).transferFrom(grantor.address, userOne.address, tokenId) - await expect( - RolesRegistry.connect(operator).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.revertedWith(`RolesRegistry: account must be token owner`) - }) - }) - describe('Non-Revocable roles', async () => { - beforeEach(async () => { - await RolesRegistry.connect(operator).grantRoleFrom(roleAssignment) - }) - it('should revoke role from if operator is only approved by grantee', async () => { - await RolesRegistry.connect(grantor).setRoleApprovalForAll(mockERC721.address, operator.address, false) - await RolesRegistry.connect(userOne).setRoleApprovalForAll(mockERC721.address, operator.address, true) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(true) - await expect( - RolesRegistry.connect(operator).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ) - .to.emit(RolesRegistry, 'RoleRevoked') - .withArgs(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address, userOne.address) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(false) - }) - it('should revoke role from if operator is approved by both grantor and grantee', async () => { - await RolesRegistry.connect(grantor).setRoleApprovalForAll(mockERC721.address, operator.address, true) - await RolesRegistry.connect(userOne).setRoleApprovalForAll(mockERC721.address, operator.address, true) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(true) - await expect( - RolesRegistry.connect(operator).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ) - .to.emit(RolesRegistry, 'RoleRevoked') - .withArgs(PROPERTY_MANAGER, mockERC721.address, tokenId, grantor.address, userOne.address) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.equal(false) - }) - it('should NOT revoke role from if operator is only approved by grantor', async () => { - await RolesRegistry.connect(grantor).setRoleApprovalForAll(mockERC721.address, operator.address, true) - await expect( - RolesRegistry.connect(operator).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userOne.address, - ), - ).to.be.revertedWith(`RolesRegistry: Role is not revocable or caller is not the grantee`) - }) - }) - }) - }) - }) - - describe('Transfers', async function () { - beforeEach(async function () { - roleAssignment.grantee = userTwo.address - await RolesRegistry.connect(grantor).grantRevocableRoleFrom(roleAssignment) - - await mockERC721.connect(grantor).transferFrom(grantor.address, userTwo.address, tokenId) - }) - it('Should keep the role when transferring the NFT', async function () { - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userTwo.address, - ), - ).to.be.equal(true) - }) - it('Should revoke the role after transferring the NFT', async function () { - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userTwo.address, - ), - ).to.be.equal(true) - - await RolesRegistry.connect(userTwo).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - userTwo.address, - userTwo.address, - ) - - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userTwo.address, - ), - ).to.be.equal(false) - }) - it('Should NOT revoke role from if operator is only approved by previous NFT owner', async () => { - await RolesRegistry.connect(grantor).setRoleApprovalForAll(mockERC721.address, userOne.address, true) - await expect( - RolesRegistry.connect(userOne).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userTwo.address, - ), - ).to.be.revertedWith(`RolesRegistry: account must be token owner`) - }) - it('Should revoke role from if operator is approved by grantee', async () => { - await RolesRegistry.connect(userTwo).setRoleApprovalForAll(mockERC721.address, userOne.address, true) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userTwo.address, - ), - ).to.be.equal(true) - await expect( - RolesRegistry.connect(userOne).revokeRoleFrom( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - userTwo.address, - userTwo.address, - ), - ) - .to.emit(RolesRegistry, 'RoleRevoked') - .withArgs(PROPERTY_MANAGER, mockERC721.address, tokenId, userTwo.address, userTwo.address) - expect( - await RolesRegistry.hasNonUniqueRole( - PROPERTY_MANAGER, - mockERC721.address, - tokenId, - grantor.address, - userTwo.address, - ), - ).to.be.equal(false) - }) - }) - }) -}) diff --git a/test/SftRolesRegistry/SftRolesRegistry.spec.ts b/test/SftRolesRegistry/SftRolesRegistry.spec.ts index 1d5215a..cc258e1 100644 --- a/test/SftRolesRegistry/SftRolesRegistry.spec.ts +++ b/test/SftRolesRegistry/SftRolesRegistry.spec.ts @@ -5,11 +5,9 @@ import { expect } from 'chai' import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { Commitment, GrantRoleData } from './types' -import { generateRandomInt } from '../helpers' +import { generateRandomInt, ONE_DAY, generateRoleId } from '../helpers' import { assertCreateCommitmentEvent, assertGrantRoleEvent, assertRevokeRoleEvent } from './helpers/assertEvents' import { - ONE_DAY, - generateRoleId, buildCommitment, buildGrantRole, getSftRolesRegistryInterfaceId, diff --git a/test/SftRolesRegistry/SftRolesRegistrySingleRole.spec.ts b/test/SftRolesRegistry/SftRolesRegistrySingleRole.spec.ts index dd27d7f..2dc484a 100644 --- a/test/SftRolesRegistry/SftRolesRegistrySingleRole.spec.ts +++ b/test/SftRolesRegistry/SftRolesRegistrySingleRole.spec.ts @@ -5,15 +5,13 @@ import { expect } from 'chai' import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { - generateRoleId, buildCommitment, buildGrantRole, getSftRolesRegistryInterfaceId, getCommitTokensAndGrantRoleInterfaceId, - ONE_DAY, } from './helpers/mockData' import { GrantRoleData, Commitment } from './types' -import { generateRandomInt } from '../helpers' +import { generateRandomInt, ONE_DAY, generateRoleId } from '../helpers' import { assertCreateCommitmentEvent, assertGrantRoleEvent, assertRevokeRoleEvent } from './helpers/assertEvents' describe('SftRolesRegistrySingleRole', async () => { diff --git a/test/SftRolesRegistry/helpers/assertEvents.ts b/test/SftRolesRegistry/helpers/assertEvents.ts index 8d877ad..623aee4 100644 --- a/test/SftRolesRegistry/helpers/assertEvents.ts +++ b/test/SftRolesRegistry/helpers/assertEvents.ts @@ -2,6 +2,7 @@ import { buildGrantRole, buildCommitment } from './mockData' import { expect } from 'chai' import { Contract } from 'ethers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { generateRoleId } from '../../helpers' export async function assertCreateCommitmentEvent( SftRolesRegistry: Contract, @@ -61,7 +62,7 @@ export async function assertGrantRoleEvent( commitmentId, grantee, revocable, - role, + role: role ? generateRoleId(role) : undefined, }) if (anotherUser) { const tokenAddress = await SftRolesRegistry.tokenAddressOf(commitmentId) diff --git a/test/SftRolesRegistry/helpers/mockData.ts b/test/SftRolesRegistry/helpers/mockData.ts index bc844a5..7eedf5e 100644 --- a/test/SftRolesRegistry/helpers/mockData.ts +++ b/test/SftRolesRegistry/helpers/mockData.ts @@ -1,14 +1,12 @@ -import { solidityKeccak256 } from 'ethers/lib/utils' -import { GrantRoleData, Commitment, RoleAssignment } from '../types' -import { generateRandomInt } from '../../helpers' +import { GrantRoleData, Commitment } from '../types' import { time } from '@nomicfoundation/hardhat-network-helpers' import { ethers } from 'ethers' import { ISftRolesRegistry__factory } from '../../../typechain-types' import { ICommitTokensAndGrantRoleExtension__factory } from '../../../typechain-types' import { IRoleBalanceOfExtension__factory } from '../../../typechain-types' +import { ONE_DAY, ROLE, generateRandomInt, generateErc165InterfaceId } from '../../helpers' const { HashZero, AddressZero } = ethers.constants -export const ONE_DAY = 60 * 60 * 24 export function buildCommitment({ grantor = AddressZero, @@ -21,7 +19,7 @@ export function buildCommitment({ export async function buildGrantRole({ commitmentId = generateRandomInt(), - role = 'UNIQUE_ROLE', + role = ROLE, grantee = AddressZero, expirationDate = null, revocable = true, @@ -29,50 +27,7 @@ export async function buildGrantRole({ }): Promise { return { commitmentId, - role: generateRoleId(role), - grantee, - expirationDate: expirationDate ? expirationDate : (await time.latest()) + ONE_DAY, - revocable, - data, - } -} - -export function generateRoleId(role: string) { - return solidityKeccak256(['string'], [role]) -} - -export async function buildRoleAssignment({ - // default values - itemId = generateRandomInt(), - role = 'UNIQUE_ROLE', - tokenAddress = AddressZero, - tokenId = generateRandomInt(), - tokenAmount = generateRandomInt(), - grantor = AddressZero, - grantee = AddressZero, - expirationDate = null, - revocable = true, - data = HashZero, -}: { - // types - itemId?: number - role?: string - tokenAddress?: string - tokenId?: number - tokenAmount?: number - grantor?: string - grantee?: string - expirationDate?: number | null - revocable?: boolean - data?: string -} = {}): Promise { - return { - itemId, - role: generateRoleId(role), - tokenAddress, - tokenId, - tokenAmount, - grantor, + role, grantee, expirationDate: expirationDate ? expirationDate : (await time.latest()) + ONE_DAY, revocable, @@ -94,12 +49,3 @@ export function getRoleBalanceOfInterfaceId() { const iface = IRoleBalanceOfExtension__factory.createInterface() return generateErc165InterfaceId(iface) } - -function generateErc165InterfaceId(contractInterface: ethers.utils.Interface) { - let interfaceID = ethers.constants.Zero - const functions: string[] = Object.keys(contractInterface.functions).filter(f => f !== 'supportsInterface(bytes4)') - for (let i = 0; i < functions.length; i++) { - interfaceID = interfaceID.xor(contractInterface.getSighash(functions[i])) - } - return interfaceID -} diff --git a/test/SftRolesRegistry/types.ts b/test/SftRolesRegistry/types.ts index 7ad0e2c..7f51396 100644 --- a/test/SftRolesRegistry/types.ts +++ b/test/SftRolesRegistry/types.ts @@ -13,25 +13,3 @@ export interface GrantRoleData { revocable: boolean data: string } - -export interface RoleAssignment { - itemId: number - role: string - tokenAddress: string - tokenId: number - tokenAmount: number - grantor: string - grantee: string - expirationDate: number - revocable: boolean - data: string -} - -export interface RevokeRoleData { - itemId: number - role: string - tokenAddress: string - tokenId: number - grantor: string - grantee: string -} diff --git a/test/helpers.ts b/test/helpers.ts index 3aacbf7..2e60a83 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,5 +1,9 @@ -import { Contract } from 'ethers' +import { Contract, ethers } from 'ethers' import { expect } from 'chai' +import { solidityKeccak256 } from 'ethers/lib/utils' + +export const ONE_DAY = 60 * 60 * 24 +export const ROLE = generateRoleId('UNIQUE_ROLE') /** * Validates the list length, order, head and tail @@ -114,3 +118,16 @@ export async function printList(LinkedLists: Contract, listId: string) { export function generateRandomInt() { return Math.floor(Math.random() * 1000 * 1000) + 1 } + +export function generateRoleId(role: string) { + return solidityKeccak256(['string'], [role]) +} + +export function generateErc165InterfaceId(contractInterface: ethers.utils.Interface) { + let interfaceID = ethers.constants.Zero + const functions: string[] = Object.keys(contractInterface.functions).filter(f => f !== 'supportsInterface(bytes4)') + for (let i = 0; i < functions.length; i++) { + interfaceID = interfaceID.xor(contractInterface.getSighash(functions[i])) + } + return interfaceID +} diff --git a/test/types.ts b/test/types.ts index a8ef3f4..ddaa78e 100644 --- a/test/types.ts +++ b/test/types.ts @@ -1,28 +1,9 @@ -export interface NftMetadata { - name: string - description: string - roles: Role[] -} - export interface Role { - name: string - description: string - isUniqueRole: boolean - inputs: Input[] -} - -export interface Input { - name: string - type: string - components?: Input[] -} - -export interface RoleAssignment { - role: string tokenAddress: string tokenId: number - grantor: string - grantee: string + roleId: string + recipient: string expirationDate: number + revocable: boolean data: string }