From 9f51b6246bf9dd2a294fb3553f78378333ba2a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernani=20S=C3=A3o=20Thiago?= Date: Wed, 15 May 2024 15:15:29 -0600 Subject: [PATCH] ON-846: ERC-7432 roles registry with linked list (#31) ON-846: ERC-7432 roles registry with linked list --- .../ERC7432/ERC7432ImmutableRegistry.sol | 238 ++++++++++ test/ERC7432/ERC7432ImmutableRegistry.spec.ts | 448 ++++++++++++++++++ 2 files changed, 686 insertions(+) create mode 100644 contracts/ERC7432/ERC7432ImmutableRegistry.sol create mode 100644 test/ERC7432/ERC7432ImmutableRegistry.spec.ts diff --git a/contracts/ERC7432/ERC7432ImmutableRegistry.sol b/contracts/ERC7432/ERC7432ImmutableRegistry.sol new file mode 100644 index 0000000..5de7169 --- /dev/null +++ b/contracts/ERC7432/ERC7432ImmutableRegistry.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +import { IERC7432 } from '../interfaces/IERC7432.sol'; +import { Uint64SortedLinkedListLibrary } from '../libraries/Uint64SortedLinkedListLibrary.sol'; +import { IERC721 } from '@openzeppelin/contracts/token/ERC721/IERC721.sol'; +import { IERC721Receiver } from '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol'; +import { ERC721Holder } from '@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol'; + +contract ERC7432ImmutableRegistry is IERC7432, ERC721Holder { + using Uint64SortedLinkedListLibrary for Uint64SortedLinkedListLibrary.List; + + 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; + + // tokenAddress => tokenId => List + mapping(address => mapping(uint256 => Uint64SortedLinkedListLibrary.List)) public roleExpirationDates; + + /** External Functions **/ + + function grantRole(IERC7432.Role calldata _role) external override { + require( + _role.expirationDate > block.timestamp, + 'ERC7432ImmutableRegistry: 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]; + if (!_roleData.revocable) { + require( + _roleData.expirationDate < block.timestamp, + 'ERC7432ImmutableRegistry: non-revocable role is not expired' + ); + roleExpirationDates[_role.tokenAddress][_role.tokenId].remove(_roleData.expirationDate); + } + + roles[_role.tokenAddress][_role.tokenId][_role.roleId] = RoleData( + _role.recipient, + _role.expirationDate, + _role.revocable, + _role.data + ); + + // add expiration date to the list if the role is not revocable + // this list prevents the owner of unlocking the token before all non-revocable roles are expired + if (!_role.revocable) { + roleExpirationDates[_role.tokenAddress][_role.tokenId].insert(_role.expirationDate); + } + + 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 { + require( + roles[_tokenAddress][_tokenId][_roleId].expirationDate != 0, + 'ERC7432ImmutableRegistry: role does not exist' + ); + + 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, + 'ERC7432ImmutableRegistry: role is not revocable nor expired' + ); + } + + // remove expiration date from the list if the role is not revocable + if (!roles[_tokenAddress][_tokenId][_roleId].revocable) { + roleExpirationDates[_tokenAddress][_tokenId].remove(roles[_tokenAddress][_tokenId][_roleId].expirationDate); + } + + delete roles[_tokenAddress][_tokenId][_roleId]; + emit RoleRevoked(_tokenAddress, _tokenId, _roleId); + } + + function unlockToken(address _tokenAddress, uint256 _tokenId) external override { + address originalOwner = originalOwners[_tokenAddress][_tokenId]; + require( + originalOwner == msg.sender || isRoleApprovedForAll(_tokenAddress, originalOwner, msg.sender), + 'ERC7432ImmutableRegistry: sender must be owner or approved' + ); + + // only allow unlocking if all non-revocable roles are expired + // since the list is sorted, the head is the highest expiration date + uint64 highestNonRevocableExpirationDate = roleExpirationDates[_tokenAddress][_tokenId].head; + require(block.timestamp > highestNonRevocableExpirationDate, 'ERC7432ImmutableRegistry: NFT is locked'); + + delete originalOwners[_tokenAddress][_tokenId]; + IERC721(_tokenAddress).transferFrom(address(this), originalOwner, _tokenId); + emit TokenUnlocked(originalOwner, _tokenAddress, _tokenId); + } + + 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 ownerOf(address _tokenAddress, uint256 _tokenId) external view returns (address owner_) { + return originalOwners[_tokenAddress][_tokenId]; + } + + function recipientOf( + address _tokenAddress, + uint256 _tokenId, + bytes32 _roleId + ) external view returns (address recipient_) { + if (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 (roles[_tokenAddress][_tokenId][_roleId].expirationDate > block.timestamp) { + return roles[_tokenAddress][_tokenId][_roleId].data; + } + return ''; + } + + function roleExpirationDate( + address _tokenAddress, + uint256 _tokenId, + bytes32 _roleId + ) external view returns (uint64 expirationDate_) { + if (roles[_tokenAddress][_tokenId][_roleId].expirationDate > block.timestamp) { + return roles[_tokenAddress][_tokenId][_roleId].expirationDate; + } + return 0; + } + + function isRoleRevocable( + address _tokenAddress, + uint256 _tokenId, + bytes32 _roleId + ) external view returns (bool revocable_) { + if (roles[_tokenAddress][_tokenId][_roleId].expirationDate > block.timestamp) { + return roles[_tokenAddress][_tokenId][_roleId].revocable; + } + return false; + } + + function isRoleApprovedForAll(address _tokenAddress, address _owner, address _operator) public view returns (bool) { + return tokenApprovals[_owner][_tokenAddress][_operator]; + } + + /** ERC-165 Functions **/ + + function supportsInterface(bytes4 interfaceId) external view virtual override returns (bool) { + return interfaceId == type(IERC7432).interfaceId || interfaceId == type(IERC721Receiver).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), + 'ERC7432ImmutableRegistry: 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), + 'ERC7432ImmutableRegistry: sender must be owner or approved' + ); + IERC721(_tokenAddress).transferFrom(_currentOwner, address(this), _tokenId); + originalOwners[_tokenAddress][_tokenId] = _currentOwner; + originalOwner_ = _currentOwner; + emit TokenLocked(_currentOwner, _tokenAddress, _tokenId); + } + } + + /// @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('ERC7432ImmutableRegistry: sender is not approved'); + } +} diff --git a/test/ERC7432/ERC7432ImmutableRegistry.spec.ts b/test/ERC7432/ERC7432ImmutableRegistry.spec.ts new file mode 100644 index 0000000..d3022e2 --- /dev/null +++ b/test/ERC7432/ERC7432ImmutableRegistry.spec.ts @@ -0,0 +1,448 @@ +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, IERC721Receiver__factory } from '../../typechain-types' +import { generateErc165InterfaceId, ONE_DAY } from '../helpers' + +const { AddressZero, HashZero } = ethers.constants + +describe('ERC7432ImmutableRegistry', () => { + let ERC7432ImmutableRegistry: Contract + let MockErc721Token: Contract + let owner: SignerWithAddress + let recipient: SignerWithAddress + let anotherUser: SignerWithAddress + let role: Role + + async function deployContracts() { + const SftRolesRegistryFactory = await ethers.getContractFactory('ERC7432ImmutableRegistry') + ERC7432ImmutableRegistry = 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 = role.recipient, revocable = role.revocable }) { + await MockErc721Token.approve(ERC7432ImmutableRegistry.address, role.tokenId) + await expect(ERC7432ImmutableRegistry.grantRole({ ...role, recipient, revocable })) + .to.emit(ERC7432ImmutableRegistry, 'RoleGranted') + .withArgs( + role.tokenAddress, + role.tokenId, + role.roleId, + owner.address, + recipient, + role.expirationDate, + revocable, + role.data, + ) + .to.emit(MockErc721Token, 'Transfer') + .withArgs(owner.address, ERC7432ImmutableRegistry.address, role.tokenId) + .to.emit(ERC7432ImmutableRegistry, 'TokenLocked') + .withArgs(owner.address, role.tokenAddress, 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(ERC7432ImmutableRegistry.connect(owner).grantRole(role)).to.be.revertedWith( + 'ERC7432ImmutableRegistry: expiration date must be in the future', + ) + }) + + it('should revert when tokenAddress is not an ERC-721', async () => { + const role = await buildRole({}) + await expect(ERC7432ImmutableRegistry.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(ERC7432ImmutableRegistry.connect(recipient).grantRole(role)).to.be.revertedWith( + 'ERC7432ImmutableRegistry: sender must be owner or approved', + ) + }) + + it('should revert when contract is not approved to transfer NFT', async () => { + await expect(ERC7432ImmutableRegistry.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 ERC7432ImmutableRegistry.connect(owner).setRoleApprovalForAll( + role.tokenAddress, + anotherUser.address, + true, + ) + await expect(ERC7432ImmutableRegistry.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(ERC7432ImmutableRegistry.address, role.tokenId) + await ERC7432ImmutableRegistry.connect(owner).setRoleApprovalForAll( + role.tokenAddress, + anotherUser.address, + true, + ) + await expect(ERC7432ImmutableRegistry.connect(anotherUser).grantRole(role)) + .to.emit(ERC7432ImmutableRegistry, '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, ERC7432ImmutableRegistry.address, role.tokenId) + .to.emit(ERC7432ImmutableRegistry, 'TokenLocked') + .withArgs(owner.address, role.tokenAddress, role.tokenId) + }) + }) + + describe('when NFT is deposited', () => { + beforeEach(async () => { + await depositNftAndGrantRole({}) + }) + + it('should revert when sender is not approved nor original owner', async () => { + await expect(ERC7432ImmutableRegistry.connect(anotherUser).grantRole(role)).to.be.revertedWith( + 'ERC7432ImmutableRegistry: sender must be owner or approved', + ) + }) + + it('should grant role when sender is original owner', async () => { + await expect(ERC7432ImmutableRegistry.connect(owner).grantRole(role)) + .to.emit(ERC7432ImmutableRegistry, 'RoleGranted') + .withArgs( + role.tokenAddress, + role.tokenId, + role.roleId, + owner.address, + role.recipient, + role.expirationDate, + role.revocable, + role.data, + ) + .to.not.emit(MockErc721Token, 'Transfer') + .to.not.emit(ERC7432ImmutableRegistry, 'TokenLocked') + }) + + it('should grant role when sender is approved', async () => { + await ERC7432ImmutableRegistry.connect(owner).setRoleApprovalForAll( + role.tokenAddress, + anotherUser.address, + true, + ) + await expect(ERC7432ImmutableRegistry.connect(anotherUser).grantRole(role)) + .to.emit(ERC7432ImmutableRegistry, 'RoleGranted') + .withArgs( + role.tokenAddress, + role.tokenId, + role.roleId, + owner.address, + role.recipient, + role.expirationDate, + role.revocable, + role.data, + ) + .to.not.emit(MockErc721Token, 'Transfer') + .to.not.emit(ERC7432ImmutableRegistry, 'TokenLocked') + }) + + it('should revert when there is a non-expired and non-revocable role', async () => { + await ERC7432ImmutableRegistry.connect(owner).grantRole({ ...role, revocable: false }) + await expect(ERC7432ImmutableRegistry.connect(owner).grantRole(role)).to.be.revertedWith( + 'ERC7432ImmutableRegistry: non-revocable role is not expired', + ) + }) + }) + }) + + describe('revokeRole', () => { + beforeEach(async () => { + await depositNftAndGrantRole({ recipient: recipient.address }) + }) + + it('should revert when role does not exist', async () => { + await expect( + ERC7432ImmutableRegistry.revokeRole(role.tokenAddress, role.tokenId + 1, role.roleId), + ).to.be.revertedWith('ERC7432ImmutableRegistry: role does not exist') + }) + + it('should revert when sender is not owner, recipient or approved', async () => { + await expect( + ERC7432ImmutableRegistry.connect(anotherUser).revokeRole(role.tokenAddress, role.tokenId, role.roleId), + ).to.be.revertedWith('ERC7432ImmutableRegistry: sender is not approved') + }) + + it('should revert when sender is owner but role is not revocable nor expired', async () => { + await expect(ERC7432ImmutableRegistry.connect(owner).grantRole({ ...role, revocable: false })) + + await expect( + ERC7432ImmutableRegistry.connect(owner).revokeRole(role.tokenAddress, role.tokenId, role.roleId), + ).to.be.revertedWith('ERC7432ImmutableRegistry: role is not revocable nor expired') + }) + + it('should revoke role when sender is recipient', async () => { + await expect(ERC7432ImmutableRegistry.connect(recipient).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(ERC7432ImmutableRegistry, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + }) + + it('should revoke role when sender is approved by recipient', async () => { + await ERC7432ImmutableRegistry.connect(recipient).setRoleApprovalForAll( + role.tokenAddress, + anotherUser.address, + true, + ) + await expect( + ERC7432ImmutableRegistry.connect(anotherUser).revokeRole(role.tokenAddress, role.tokenId, role.roleId), + ) + .to.emit(ERC7432ImmutableRegistry, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + }) + + it('should revoke role when sender is owner (and role is revocable)', async () => { + await expect(ERC7432ImmutableRegistry.connect(owner).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(ERC7432ImmutableRegistry, '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(ERC7432ImmutableRegistry.connect(owner).grantRole({ ...role, revocable: false })) + .to.emit(ERC7432ImmutableRegistry, 'RoleGranted') + .withArgs( + role.tokenAddress, + role.tokenId, + role.roleId, + owner.address, + role.recipient, + role.expirationDate, + false, + role.data, + ) + .to.not.emit(MockErc721Token, 'Transfer') + .to.not.emit(ERC7432ImmutableRegistry, 'TokenLocked') + + await time.increase(ONE_DAY) + await expect(ERC7432ImmutableRegistry.connect(owner).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(ERC7432ImmutableRegistry, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + }) + + it('should revoke role when sender is approved by owner (and role is revocable)', async () => { + await ERC7432ImmutableRegistry.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await expect( + ERC7432ImmutableRegistry.connect(anotherUser).revokeRole(role.tokenAddress, role.tokenId, role.roleId), + ) + .to.emit(ERC7432ImmutableRegistry, '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( + ERC7432ImmutableRegistry.connect(owner).grantRole({ + ...role, + recipient: recipient.address, + revocable: false, + }), + ) + await ERC7432ImmutableRegistry.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await ERC7432ImmutableRegistry.connect(recipient).setRoleApprovalForAll( + role.tokenAddress, + anotherUser.address, + true, + ) + await expect( + ERC7432ImmutableRegistry.connect(anotherUser).revokeRole(role.tokenAddress, role.tokenId, role.roleId), + ) + .to.emit(ERC7432ImmutableRegistry, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + }) + + it('should not delete original owner when revoking role', async () => { + await expect(ERC7432ImmutableRegistry.connect(owner).revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(ERC7432ImmutableRegistry, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + + expect(await ERC7432ImmutableRegistry.originalOwners(role.tokenAddress, role.tokenId)).to.be.equal(owner.address) + }) + + it('should revert if role was already revoked', async () => { + await expect(ERC7432ImmutableRegistry.revokeRole(role.tokenAddress, role.tokenId, role.roleId)) + .to.emit(ERC7432ImmutableRegistry, 'RoleRevoked') + .withArgs(role.tokenAddress, role.tokenId, role.roleId) + + await expect( + ERC7432ImmutableRegistry.revokeRole(role.tokenAddress, role.tokenId, role.roleId), + ).to.be.revertedWith('ERC7432ImmutableRegistry: role does not exist') + }) + }) + + describe('unlockToken', () => { + beforeEach(async () => { + await depositNftAndGrantRole({ recipient: recipient.address }) + }) + + it('should revert if token is not deposited', async () => { + await expect( + ERC7432ImmutableRegistry.connect(owner).unlockToken(role.tokenAddress, role.tokenId + 1), + ).to.be.revertedWith('ERC7432ImmutableRegistry: sender must be owner or approved') + }) + + it('should revert if sender is not original owner or approved', async () => { + await expect( + ERC7432ImmutableRegistry.connect(anotherUser).unlockToken(role.tokenAddress, role.tokenId), + ).to.be.revertedWith('ERC7432ImmutableRegistry: sender must be owner or approved') + }) + + it('should revert if NFT is locked', async () => { + const revocable = false + await expect(ERC7432ImmutableRegistry.grantRole({ ...role, revocable })) + .to.emit(ERC7432ImmutableRegistry, 'RoleGranted') + .withArgs( + role.tokenAddress, + role.tokenId, + role.roleId, + owner.address, + role.recipient, + role.expirationDate, + revocable, + role.data, + ) + .to.not.emit(MockErc721Token, 'Transfer') + .to.not.emit(ERC7432ImmutableRegistry, 'TokenLocked') + + await expect(ERC7432ImmutableRegistry.unlockToken(role.tokenAddress, role.tokenId)).to.be.revertedWith( + 'ERC7432ImmutableRegistry: NFT is locked', + ) + }) + + it('should unlock token if sender is owner and NFT is not locked', async () => { + await expect(ERC7432ImmutableRegistry.connect(owner).unlockToken(role.tokenAddress, role.tokenId)) + .to.emit(ERC7432ImmutableRegistry, 'TokenUnlocked') + .withArgs(owner.address, role.tokenAddress, role.tokenId) + .to.emit(MockErc721Token, 'Transfer') + .withArgs(ERC7432ImmutableRegistry.address, owner.address, role.tokenId) + }) + + it('should unlock token if sender is approved and NFT is not locked', async () => { + await ERC7432ImmutableRegistry.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + await expect(ERC7432ImmutableRegistry.connect(anotherUser).unlockToken(role.tokenAddress, role.tokenId)) + .to.emit(ERC7432ImmutableRegistry, 'TokenUnlocked') + .withArgs(owner.address, role.tokenAddress, role.tokenId) + .to.emit(MockErc721Token, 'Transfer') + .withArgs(ERC7432ImmutableRegistry.address, owner.address, role.tokenId) + }) + }) + + describe('view functions', async () => { + describe('when NFT is not deposited', async () => { + it('recipientOf should return default value', async () => { + expect(await ERC7432ImmutableRegistry.recipientOf(role.tokenAddress, role.tokenId, role.roleId)).to.be.equal( + AddressZero, + ) + }) + + it('roleData should return default value', async () => { + expect(await ERC7432ImmutableRegistry.roleData(role.tokenAddress, role.tokenId, role.roleId)).to.be.equal('0x') + }) + + it('roleExpirationDate should return default value', async () => { + expect( + await ERC7432ImmutableRegistry.roleExpirationDate(role.tokenAddress, role.tokenId, role.roleId), + ).to.be.equal(0) + }) + + it('isRoleRevocable should return default value', async () => { + expect(await ERC7432ImmutableRegistry.isRoleRevocable(role.tokenAddress, role.tokenId, role.roleId)).to.be.false + }) + }) + + describe('when NFT is deposited', async () => { + beforeEach(async () => { + await depositNftAndGrantRole({ recipient: recipient.address }) + }) + + it('ownerOf should return value from mapping', async () => { + expect(await ERC7432ImmutableRegistry.ownerOf(role.tokenAddress, role.tokenId)).to.be.equal(owner.address) + }) + + it('recipientOf should return value from mapping', async () => { + expect(await ERC7432ImmutableRegistry.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 ERC7432ImmutableRegistry.roleData(role.tokenAddress, role.tokenId, role.roleId)).to.be.equal( + HashZero, + ) + }) + + it('roleExpirationDate should the expiration date of the role', async () => { + expect( + await ERC7432ImmutableRegistry.roleExpirationDate(role.tokenAddress, role.tokenId, role.roleId), + ).to.be.equal(role.expirationDate) + }) + + it('isRoleRevocable should whether the role is revocable', async () => { + expect(await ERC7432ImmutableRegistry.isRoleRevocable(role.tokenAddress, role.tokenId, role.roleId)).to.be.true + }) + }) + }) + + describe('isRoleApprovedForAll', async () => { + it('should return false when not approved', async () => { + expect(await ERC7432ImmutableRegistry.isRoleApprovedForAll(role.tokenAddress, owner.address, anotherUser.address)) + .to.be.false + }) + + it('should return true when approved', async () => { + await ERC7432ImmutableRegistry.connect(owner).setRoleApprovalForAll(role.tokenAddress, anotherUser.address, true) + expect(await ERC7432ImmutableRegistry.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 ERC7432ImmutableRegistry.supportsInterface(ifaceId)).to.be.true + }) + + it('should return true when IERC721Receiver identifier is provided', async () => { + const iface = IERC721Receiver__factory.createInterface() + const ifaceId = generateErc165InterfaceId(iface) + expect(await ERC7432ImmutableRegistry.supportsInterface(ifaceId)).to.be.true + }) + }) +})