From 6e3e9c9eb7f3d5193b28cedbf3632ca7b4f7f7b3 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Thu, 4 Jul 2024 12:21:47 -0300 Subject: [PATCH 01/13] feat: update marketplace functions calls with the new Interface --- contracts/OriumSftMarketplace.sol | 8 +- contracts/interfaces/IERC7589.sol | 178 ++++++++--------- contracts/interfaces/IERC7589Legacy.sol | 182 ++++++++++++++++++ .../libraries/LibOriumSftMarketplace.sol | 12 +- 4 files changed, 268 insertions(+), 112 deletions(-) create mode 100644 contracts/interfaces/IERC7589Legacy.sol diff --git a/contracts/OriumSftMarketplace.sol b/contracts/OriumSftMarketplace.sol index 44a1c13..eebcdd6 100644 --- a/contracts/OriumSftMarketplace.sol +++ b/contracts/OriumSftMarketplace.sol @@ -128,7 +128,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra _validateCreateRentalOffer(_offer, _rolesRegistryAddress); if (_offer.commitmentId == 0) { - _offer.commitmentId = IERC7589(_rolesRegistryAddress).commitTokens( + _offer.commitmentId = IERC7589(_rolesRegistryAddress).lockTokens( _offer.lender, _offer.tokenAddress, _offer.tokenId, @@ -221,7 +221,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra rentals[_offerHash].expirationDate < block.timestamp && _rolesRegistry.tokenAmountOf(_offer.commitmentId) > 0 ) { - _rolesRegistry.releaseTokens(_offer.commitmentId); + _rolesRegistry.unlockTokens(_offer.commitmentId); } nonceDeadline[msg.sender][_offer.nonce] = uint64(block.timestamp); @@ -245,7 +245,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra IERC7589 _rolesRegistry = IERC7589( IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) ); - _rolesRegistry.releaseTokens(_offer.commitmentId); + _rolesRegistry.unlockTokens(_offer.commitmentId); } /** @@ -304,7 +304,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra IERC7589 _rolesRegistry = IERC7589(_rolesRegistryAddress); uint256 _validCommitmentId = _params[i].commitmentId; if (_params[i].commitmentId == 0) { - _validCommitmentId = _rolesRegistry.commitTokens( + _validCommitmentId = _rolesRegistry.lockTokens( msg.sender, _params[i].tokenAddress, _params[i].tokenId, diff --git a/contracts/interfaces/IERC7589.sol b/contracts/interfaces/IERC7589.sol index 202a306..b0b5f1b 100644 --- a/contracts/interfaces/IERC7589.sol +++ b/contracts/interfaces/IERC7589.sol @@ -2,64 +2,53 @@ pragma solidity 0.8.9; -import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { IERC165 } from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; +/// @title ERC-7589 Semi-Fungible Token Roles +/// @dev See https://eips.ethereum.org/EIPS/eip-7589 +/// Note: the ERC-165 identifier for this interface is 0x6f831543. interface IERC7589 is IERC165 { - struct RoleAssignment { - address grantee; - uint64 expirationDate; - bool revocable; - bytes data; - } - - struct Commitment { - address grantor; - address tokenAddress; - uint256 tokenId; - uint256 tokenAmount; - } - /** Events **/ - /// @notice Emitted when tokens are committed (deposited or frozen). - /// @param _grantor The owner of the SFTs. - /// @param _commitmentId The identifier of the commitment created. + /// @notice Emitted when tokens are locked (deposited or frozen). + /// @param _owner The owner of the tokens. + /// @param _lockId The identifier of the locked tokens. /// @param _tokenAddress The token address. /// @param _tokenId The token identifier. /// @param _tokenAmount The token amount. - event TokensCommitted( - address indexed _grantor, - uint256 indexed _commitmentId, + event TokensLocked( + address indexed _owner, + uint256 indexed _lockId, address indexed _tokenAddress, uint256 _tokenId, uint256 _tokenAmount ); /// @notice Emitted when a role is granted. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient the role. + /// @param _lockId The identifier of the locked tokens. + /// @param _roleId The role identifier. + /// @param _recipient The recipient the role. /// @param _expirationDate The expiration date of the role. - /// @param _revocable Whether the role is revocable or not. + /// @param _revocable Whether the role is revocable. /// @param _data Any additional data about the role. event RoleGranted( - uint256 indexed _commitmentId, - bytes32 indexed _role, - address indexed _grantee, + uint256 indexed _lockId, + bytes32 indexed _roleId, + address indexed _recipient, uint64 _expirationDate, bool _revocable, bytes _data ); /// @notice Emitted when a role is revoked. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient of the role revocation. - event RoleRevoked(uint256 indexed _commitmentId, bytes32 indexed _role, address indexed _grantee); + /// @param _lockId The identifier of the locked tokens. + /// @param _roleId The role identifier. + /// @param _recipient The recipient of the role revocation. + event RoleRevoked(uint256 indexed _lockId, bytes32 indexed _roleId, address indexed _recipient); - /// @notice Emitted when a user releases tokens from a commitment. - /// @param _commitmentId The commitment identifier. - event TokensReleased(uint256 indexed _commitmentId); + /// @notice Emitted when tokens are unlocked (withdrawn or unfrozen). + /// @param _lockId The identifier of the locked tokens. + event TokensUnlocked(uint256 indexed _lockId); /// @notice Emitted when a user is approved to manage roles on behalf of another user. /// @param _tokenAddress The token address. @@ -69,44 +58,44 @@ interface IERC7589 is IERC165 { /** External Functions **/ - /// @notice Commits tokens (deposits on a contract or freezes balance). - /// @param _grantor The owner of the SFTs. + /// @notice Lock tokens (deposits on a contract or freezes balance). + /// @param _owner The owner of the tokens. /// @param _tokenAddress The token address. /// @param _tokenId The token identifier. /// @param _tokenAmount The token amount. - /// @return commitmentId_ The unique identifier of the commitment created. - function commitTokens( - address _grantor, + /// @return lockId_ The identifier of the locked tokens. + function lockTokens( + address _owner, address _tokenAddress, uint256 _tokenId, uint256 _tokenAmount - ) external returns (uint256 commitmentId_); + ) external returns (uint256 lockId_); - /// @notice Grants a role to `_grantee`. - /// @param _commitmentId The identifier of the commitment. - /// @param _role The role identifier. - /// @param _grantee The recipient the role. + /// @notice Grants a role to a user. + /// @param _lockId The identifier of the locked tokens. + /// @param _roleId The role identifier. + /// @param _recipient The recipient the role. /// @param _expirationDate The expiration date of the role. - /// @param _revocable Whether the role is revocable or not. + /// @param _revocable Whether the role is revocable. /// @param _data Any additional data about the role. function grantRole( - uint256 _commitmentId, - bytes32 _role, - address _grantee, + uint256 _lockId, + bytes32 _roleId, + address _recipient, uint64 _expirationDate, bool _revocable, bytes calldata _data ) external; /// @notice Revokes a role. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient of the role revocation. - function revokeRole(uint256 _commitmentId, bytes32 _role, address _grantee) external; + /// @param _lockId The identifier of the locked tokens. + /// @param _roleId The role identifier. + /// @param _recipient The recipient of the role revocation. + function revokeRole(uint256 _lockId, bytes32 _roleId, address _recipient) external; - /// @notice Releases tokens back to grantor. - /// @param _commitmentId The commitment identifier. - function releaseTokens(uint256 _commitmentId) external; + /// @notice Unlocks tokens (transfer back to original owner or unfreeze it). + /// @param _lockId The identifier of the locked tokens. + function unlockTokens(uint256 _lockId) external; /// @notice Approves operator to grant and revoke roles on behalf of another user. /// @param _tokenAddress The token address. @@ -116,67 +105,52 @@ interface IERC7589 is IERC165 { /** View Functions **/ - /// @notice Returns the owner of the commitment (grantor). - /// @param _commitmentId The commitment identifier. - /// @return grantor_ The commitment owner. - function grantorOf(uint256 _commitmentId) external view returns (address grantor_); + /// @notice Retrieves the owner of the tokens. + /// @param _lockId The identifier of the locked tokens. + /// @return owner_ The owner of the tokens. + function ownerOf(uint256 _lockId) external view returns (address owner_); - /// @notice Returns the address of the token committed. - /// @param _commitmentId The commitment identifier. + /// @notice Retrieves the address of the locked tokens. + /// @param _lockId The identifier of the locked tokens. /// @return tokenAddress_ The token address. - function tokenAddressOf(uint256 _commitmentId) external view returns (address tokenAddress_); + function tokenAddressOf(uint256 _lockId) external view returns (address tokenAddress_); - /// @notice Returns the identifier of the token committed. - /// @param _commitmentId The commitment identifier. + /// @notice Retrieves the tokenId of the locked tokens. + /// @param _lockId The identifier of the locked tokens. /// @return tokenId_ The token identifier. - function tokenIdOf(uint256 _commitmentId) external view returns (uint256 tokenId_); + function tokenIdOf(uint256 _lockId) external view returns (uint256 tokenId_); - /// @notice Returns the amount of tokens committed. - /// @param _commitmentId The commitment identifier. + /// @notice Retrieves the amount of tokens locked. + /// @param _lockId The identifier of the locked tokens. /// @return tokenAmount_ The token amount. - function tokenAmountOf(uint256 _commitmentId) external view returns (uint256 tokenAmount_); + function tokenAmountOf(uint256 _lockId) external view returns (uint256 tokenAmount_); - /// @notice Returns the custom data of a role assignment. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient the role. + /// @notice Retrieves the custom data of a role. + /// @param _lockId The identifier of the locked tokens. + /// @param _roleId The role identifier. /// @return data_ The custom data. - function roleData( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) external view returns (bytes memory data_); - - /// @notice Returns the expiration date of a role assignment. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient the role. + function roleData(uint256 _lockId, bytes32 _roleId) external view returns (bytes memory data_); + + /// @notice Retrieves the expiration date of a role. + /// @param _lockId The identifier of the locked tokens. + /// @param _roleId The role identifier. /// @return expirationDate_ The expiration date. - function roleExpirationDate( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) external view returns (uint64 expirationDate_); - - /// @notice Returns the expiration date of a role assignment. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient the role. + function roleExpirationDate(uint256 _lockId, bytes32 _roleId) external view returns (uint64 expirationDate_); + + /// @notice Retrieves the expiration date of a role. + /// @param _lockId The identifier of the locked tokens. + /// @param _roleId The role identifier. /// @return revocable_ Whether the role is revocable or not. - function isRoleRevocable( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) external view returns (bool revocable_); + function isRoleRevocable(uint256 _lockId, bytes32 _roleId) external view returns (bool revocable_); - /// @notice Checks if the grantor approved the operator for all SFTs. + /// @notice Checks if the owner approved the operator for all SFTs. /// @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 isApproved_ Whether the operator is approved or not. function isRoleApprovedForAll( address _tokenAddress, - address _grantor, + address _owner, address _operator ) external view returns (bool isApproved_); -} +} \ No newline at end of file diff --git a/contracts/interfaces/IERC7589Legacy.sol b/contracts/interfaces/IERC7589Legacy.sol new file mode 100644 index 0000000..7b3fe4d --- /dev/null +++ b/contracts/interfaces/IERC7589Legacy.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +interface IERC7589Legacy is IERC165 { + struct RoleAssignment { + address grantee; + uint64 expirationDate; + bool revocable; + bytes data; + } + + struct Commitment { + address grantor; + address tokenAddress; + uint256 tokenId; + uint256 tokenAmount; + } + + /** Events **/ + + /// @notice Emitted when tokens are committed (deposited or frozen). + /// @param _grantor The owner of the SFTs. + /// @param _commitmentId The identifier of the commitment created. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _tokenAmount The token amount. + event TokensCommitted( + address indexed _grantor, + uint256 indexed _commitmentId, + address indexed _tokenAddress, + uint256 _tokenId, + uint256 _tokenAmount + ); + + /// @notice Emitted when a role is granted. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient 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( + uint256 indexed _commitmentId, + bytes32 indexed _role, + address indexed _grantee, + uint64 _expirationDate, + bool _revocable, + bytes _data + ); + + /// @notice Emitted when a role is revoked. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient of the role revocation. + event RoleRevoked(uint256 indexed _commitmentId, bytes32 indexed _role, address indexed _grantee); + + /// @notice Emitted when a user releases tokens from a commitment. + /// @param _commitmentId The commitment identifier. + event TokensReleased(uint256 indexed _commitmentId); + + /// @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); + + /** External Functions **/ + + /// @notice Commits tokens (deposits on a contract or freezes balance). + /// @param _grantor The owner of the SFTs. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _tokenAmount The token amount. + /// @return commitmentId_ The unique identifier of the commitment created. + function commitTokens( + address _grantor, + address _tokenAddress, + uint256 _tokenId, + uint256 _tokenAmount + ) external returns (uint256 commitmentId_); + + /// @notice Grants a role to `_grantee`. + /// @param _commitmentId The identifier of the commitment. + /// @param _role The role identifier. + /// @param _grantee The recipient 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. + function grantRole( + uint256 _commitmentId, + bytes32 _role, + address _grantee, + uint64 _expirationDate, + bool _revocable, + bytes calldata _data + ) external; + + /// @notice Revokes a role. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient of the role revocation. + function revokeRole(uint256 _commitmentId, bytes32 _role, address _grantee) external; + + /// @notice Releases tokens back to grantor. + /// @param _commitmentId The commitment identifier. + function releaseTokens(uint256 _commitmentId) external; + + /// @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. + function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _approved) external; + + /** View Functions **/ + + /// @notice Returns the owner of the commitment (grantor). + /// @param _commitmentId The commitment identifier. + /// @return grantor_ The commitment owner. + function grantorOf(uint256 _commitmentId) external view returns (address grantor_); + + /// @notice Returns the address of the token committed. + /// @param _commitmentId The commitment identifier. + /// @return tokenAddress_ The token address. + function tokenAddressOf(uint256 _commitmentId) external view returns (address tokenAddress_); + + /// @notice Returns the identifier of the token committed. + /// @param _commitmentId The commitment identifier. + /// @return tokenId_ The token identifier. + function tokenIdOf(uint256 _commitmentId) external view returns (uint256 tokenId_); + + /// @notice Returns the amount of tokens committed. + /// @param _commitmentId The commitment identifier. + /// @return tokenAmount_ The token amount. + function tokenAmountOf(uint256 _commitmentId) external view returns (uint256 tokenAmount_); + + /// @notice Returns the custom data of a role assignment. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient the role. + /// @return data_ The custom data. + function roleData( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) external view returns (bytes memory data_); + + /// @notice Returns the expiration date of a role assignment. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient the role. + /// @return expirationDate_ The expiration date. + function roleExpirationDate( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) external view returns (uint64 expirationDate_); + + /// @notice Returns the expiration date of a role assignment. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient the role. + /// @return revocable_ Whether the role is revocable or not. + function isRoleRevocable( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) external view returns (bool revocable_); + + /// @notice Checks if the grantor approved the operator for all SFTs. + /// @param _tokenAddress The token address. + /// @param _grantor The user that approved the operator. + /// @param _operator The user that can grant and revoke roles. + /// @return isApproved_ Whether the operator is approved or not. + function isRoleApprovedForAll( + address _tokenAddress, + address _grantor, + address _operator + ) external view returns (bool isApproved_); +} diff --git a/contracts/libraries/LibOriumSftMarketplace.sol b/contracts/libraries/LibOriumSftMarketplace.sol index 9acc682..7640621 100644 --- a/contracts/libraries/LibOriumSftMarketplace.sol +++ b/contracts/libraries/LibOriumSftMarketplace.sol @@ -102,7 +102,7 @@ library LibOriumSftMarketplace { "OriumSftMarketplace: tokenAmount provided does not match commitment's tokenAmount" ); require( - _rolesRegistry.grantorOf(_commitmentId) == _expectedGrantor, + _rolesRegistry.ownerOf(_commitmentId) == _expectedGrantor, "OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId" ); require( @@ -192,14 +192,14 @@ library LibOriumSftMarketplace { address _rolesRegistryAddress = IOriumMarketplaceRoyalties(_oriumMarketplaceRoyaltiesAddress) .sftRolesRegistryOf(_tokenAddresses[i]); require( - IERC7589(_rolesRegistryAddress).grantorOf(_commitmentIds[i]) == msg.sender, + IERC7589(_rolesRegistryAddress).ownerOf(_commitmentIds[i]) == msg.sender, "OriumSftMarketplace: sender is not the commitment's grantor" ); require( IERC7589(_rolesRegistryAddress).tokenAddressOf(_commitmentIds[i]) == _tokenAddresses[i], "OriumSftMarketplace: tokenAddress provided does not match commitment's tokenAddress" ); - IERC7589(_rolesRegistryAddress).releaseTokens(_commitmentIds[i]); + IERC7589(_rolesRegistryAddress).unlockTokens(_commitmentIds[i]); } } @@ -230,15 +230,15 @@ library LibOriumSftMarketplace { _tokenAddresses[i] ); require( - IERC7589(_rolesRegistryAddress).isRoleRevocable(_commitmentIds[i], _roles[i], _grantees[i]), + IERC7589(_rolesRegistryAddress).isRoleRevocable(_commitmentIds[i], _roles[i]), "OriumSftMarketplace: role is not revocable" ); require( - IERC7589(_rolesRegistryAddress).roleExpirationDate(_commitmentIds[i], _roles[i], _grantees[i]) > block.timestamp, + IERC7589(_rolesRegistryAddress).roleExpirationDate(_commitmentIds[i], _roles[i]) > block.timestamp, "OriumSftMarketplace: role is expired" ); require( - msg.sender == _grantees[i] || IERC7589(_rolesRegistryAddress).grantorOf(_commitmentIds[i]) == msg.sender, + msg.sender == _grantees[i] || IERC7589(_rolesRegistryAddress).ownerOf(_commitmentIds[i]) == msg.sender, "OriumSftMarketplace: sender is not the commitment's grantor or grantee" ); require( From dad63877b6e95f9a8ea07fc43157d70ef272de29 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Thu, 4 Jul 2024 18:21:25 -0300 Subject: [PATCH 02/13] feat: updating tests cases to fit with the new interface 7589/ update Fixture to add new constructor parameter --- .../ICommitTokensAndGrantRoleExtension.sol | 28 -- ...ERC7589LockTokensAndGrantRoleExtension.sol | 31 ++ contracts/mocks/IOriumWrapperManager.sol | 47 +++ .../mocks/SftRolesRegistrySingleRole.sol | 353 ++++++++++-------- .../mocks/Uint64SortedLinkedListLibrary.sol | 109 ++++++ test/OriumSftMarketplace.test.ts | 64 ++-- .../OriumMarketplaceRoyaltiesFixture.ts | 12 +- test/fixtures/OriumSftMarketplaceFixture.ts | 21 +- 8 files changed, 456 insertions(+), 209 deletions(-) delete mode 100644 contracts/interfaces/ICommitTokensAndGrantRoleExtension.sol create mode 100644 contracts/interfaces/IERC7589LockTokensAndGrantRoleExtension.sol create mode 100644 contracts/mocks/IOriumWrapperManager.sol create mode 100644 contracts/mocks/Uint64SortedLinkedListLibrary.sol diff --git a/contracts/interfaces/ICommitTokensAndGrantRoleExtension.sol b/contracts/interfaces/ICommitTokensAndGrantRoleExtension.sol deleted file mode 100644 index c45fcd3..0000000 --- a/contracts/interfaces/ICommitTokensAndGrantRoleExtension.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -pragma solidity 0.8.9; - -interface ICommitTokensAndGrantRoleExtension { - /// @notice Commits tokens and grant role in a single transaction. - /// @param _grantor The owner of the SFTs. - /// @param _tokenAddress The token address. - /// @param _tokenId The token identifier. - /// @param _tokenAmount The token amount. - /// @param _role The role identifier. - /// @param _grantee The recipient 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. - /// @return commitmentId_ The identifier of the commitment created. - function commitTokensAndGrantRole( - address _grantor, - address _tokenAddress, - uint256 _tokenId, - uint256 _tokenAmount, - bytes32 _role, - address _grantee, - uint64 _expirationDate, - bool _revocable, - bytes calldata _data - ) external returns (uint256 commitmentId_); -} diff --git a/contracts/interfaces/IERC7589LockTokensAndGrantRoleExtension.sol b/contracts/interfaces/IERC7589LockTokensAndGrantRoleExtension.sol new file mode 100644 index 0000000..90112dd --- /dev/null +++ b/contracts/interfaces/IERC7589LockTokensAndGrantRoleExtension.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +/// @title ERC-7589 Semi-Fungible Token Roles, optional lock tokens and grant role extension +/// @dev See https://eips.ethereum.org/EIPS/eip-7589 +/// Note: the ERC-165 identifier for this interface is 0x0a644ace. +interface IERC7589LockTokensAndGrantRoleExtension { + /// @notice Lock tokens and grant role in a single transaction. + /// @param _owner The owner of the tokens. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _tokenAmount The token amount. + /// @param _roleId The role identifier. + /// @param _recipient The recipient the role. + /// @param _expirationDate The expiration date of the role. + /// @param _revocable Whether the role is revocable. + /// @param _data Any additional data about the role. + /// @return lockId_ The identifier of the locked tokens. + function lockTokensAndGrantRole( + address _owner, + address _tokenAddress, + uint256 _tokenId, + uint256 _tokenAmount, + bytes32 _roleId, + address _recipient, + uint64 _expirationDate, + bool _revocable, + bytes calldata _data + ) external returns (uint256 lockId_); +} \ No newline at end of file diff --git a/contracts/mocks/IOriumWrapperManager.sol b/contracts/mocks/IOriumWrapperManager.sol new file mode 100644 index 0000000..8284d03 --- /dev/null +++ b/contracts/mocks/IOriumWrapperManager.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +interface IOriumWrapperManager { + /** External Functions **/ + + /// @notice Maps a token to a wrapped token. + /// @param _tokenAddress The token address. + /// @param _wrappedTokenAddress The wrapped token address. + function mapToken(address _tokenAddress, address _wrappedTokenAddress) external; + + /// @notice Unmaps a token (removes association from storage). + /// @param _tokenAddress The token address. + function unmapToken(address _tokenAddress) external; + + /// @notice Sets the maximum duration for a token. + /// @param _tokenAddress The token address. + /// @param _maxDuration The maximum duration. + function setMaxDuration(address _tokenAddress, uint256 _maxDuration) external; + + /// @notice Sets the marketplace address. + /// @param _marketplaceAddress The marketplace address. + function setMarketplaceAddress(address _marketplaceAddress) external; + + /** View Functions **/ + + /// @notice Gets the marketplace address of a token. + /// @param _tokenAddress The token address. + /// @return The marketplace address. + function getMarketplaceAddressOf(address _tokenAddress) external view returns (address); + + /// @notice Gets the wrapped token of a token. + /// @param _tokenAddress The token address. + /// @return The wrapped token address. + function getWrappedTokenOf(address _tokenAddress) external view returns (address); + + /// @notice Gets the original token of a wrapped token. + /// @param _wrappedTokenAddress The wrapped token address. + /// @return The original token address. + function getOriginalTokenOf(address _wrappedTokenAddress) external view returns (address); + + /// @notice Gets the maximum duration of a token. + /// @param _tokenAddress The token address. + /// @return The maximum duration. + function getMaxDurationOf(address _tokenAddress) external view returns (uint256); +} diff --git a/contracts/mocks/SftRolesRegistrySingleRole.sol b/contracts/mocks/SftRolesRegistrySingleRole.sol index ade4ace..4a5c1ac 100644 --- a/contracts/mocks/SftRolesRegistrySingleRole.sol +++ b/contracts/mocks/SftRolesRegistrySingleRole.sol @@ -2,225 +2,287 @@ pragma solidity 0.8.9; -import { IERC7589 } from '../interfaces/IERC7589.sol'; -import { ICommitTokensAndGrantRoleExtension } from '../interfaces/ICommitTokensAndGrantRoleExtension.sol'; import { IERC165 } from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; -import { IERC1155 } from '@openzeppelin/contracts/token/ERC1155/IERC1155.sol'; import { IERC1155Receiver } from '@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol'; import { ERC1155Holder, ERC1155Receiver } from '@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol'; -import { ERC165Checker } from '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol'; +import { IERC7589 } from '../interfaces/IERC7589.sol'; +import { IERC1155 } from '@openzeppelin/contracts/token/ERC1155/IERC1155.sol'; +import { IERC7589LockTokensAndGrantRoleExtension } from '../interfaces/IERC7589LockTokensAndGrantRoleExtension.sol'; +import { Uint64SortedLinkedListLibrary } from './Uint64SortedLinkedListLibrary.sol'; +import { IOriumWrapperManager } from './IOriumWrapperManager.sol'; + +contract SftRolesRegistrySingleRole is IERC7589, ERC1155Holder, IERC7589LockTokensAndGrantRoleExtension { + using Uint64SortedLinkedListLibrary for Uint64SortedLinkedListLibrary.List; + + struct TokenLock { + address owner; + address tokenAddress; + uint256 tokenId; + uint256 tokenAmount; + } + + struct Role { + address recipient; + uint64 expirationDate; + bool revocable; + bytes data; + } -// Semi-fungible token (SFT) registry with only one role (UNIQUE_ROLE) -contract SftRolesRegistrySingleRole is IERC7589, ERC1155Holder, ICommitTokensAndGrantRoleExtension { - bytes32 public constant UNIQUE_ROLE = keccak256('UNIQUE_ROLE'); + address public managerAddress; - uint256 public commitmentCount; + address public marketplaceAddress; - // grantor => tokenAddress => operator => isApproved + uint256 public lockIdCount; + + // tokenAddress => isAllowed + mapping(address => bool) public isTokenAddressAllowed; + + // lockId => TokenLock + mapping(uint256 => TokenLock) public tokenLocks; + + // lockId => roleId => Role + mapping(uint256 => mapping(bytes32 => Role)) public roles; + + // tokenAddress => tokenId => List + mapping(uint256 => Uint64SortedLinkedListLibrary.List) public tokenLockExpirationDates; + + // ownerAddress => tokenAddress => operator => isApproved mapping(address => mapping(address => mapping(address => bool))) public roleApprovals; - // commitmentId => Commitment - mapping(uint256 => Commitment) public commitments; + // supportedInterfaces => bool + mapping(bytes4 => bool) private supportedInterfaces; - // commitmentId => role => RoleAssignment - mapping(uint256 => mapping(bytes32 => RoleAssignment)) internal roleAssignments; + modifier onlyManager() { + require(msg.sender == managerAddress, 'ERC7589RolesRegistry: sender is not manager'); + _; + } + + modifier onlyAllowedTokenAddress(address _tokenAddress) { + require(isTokenAddressAllowed[_tokenAddress], 'ERC7589RolesRegistry: tokenAddress is not allowed'); + _; + } modifier onlyOwnerOrApproved(address _account, address _tokenAddress) { require( - _account == msg.sender || isRoleApprovedForAll(_tokenAddress, _account, msg.sender), - 'SftRolesRegistry: account not approved' + msg.sender == _account || isRoleApprovedForAll(_tokenAddress, _account, msg.sender), + 'ERC7589RolesRegistry: sender is not owner or approved' ); _; } - modifier sameGrantee( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) { + modifier onlyTokenLockOwnerOrApproved(uint256 _lockId) { + TokenLock storage _tokenLock = tokenLocks[_lockId]; require( - _grantee != address(0) && _grantee == roleAssignments[_commitmentId][_role].grantee, - 'SftRolesRegistry: grantee mismatch' + msg.sender == _tokenLock.owner || + isRoleApprovedForAll(_tokenLock.tokenAddress, _tokenLock.owner, msg.sender), + 'ERC7589RolesRegistry: sender is not owner or approved' ); _; } - /** External Functions **/ + constructor(address _marketplaceAddress) { + managerAddress = msg.sender; + marketplaceAddress = _marketplaceAddress; + supportedInterfaces[type(IERC7589).interfaceId] = true; + supportedInterfaces[type(IERC1155Receiver).interfaceId] = true; + supportedInterfaces[type(IERC7589LockTokensAndGrantRoleExtension).interfaceId] = true; + } - function commitTokens( - address _grantor, + /** ERC-7589 External Functions **/ + + function lockTokens( + address _owner, address _tokenAddress, uint256 _tokenId, uint256 _tokenAmount - ) external override onlyOwnerOrApproved(_grantor, _tokenAddress) returns (uint256 commitmentId_) { - require(_tokenAmount > 0, 'SftRolesRegistry: tokenAmount must be greater than zero'); - commitmentId_ = _createCommitment(_grantor, _tokenAddress, _tokenId, _tokenAmount); + ) external returns (uint256 lockId_) { + lockId_ = _lockTokens(_owner, _tokenAddress, _tokenId, _tokenAmount); } function grantRole( - uint256 _commitmentId, - bytes32 _role, - address _grantee, + uint256 _lockId, + bytes32 _roleId, + address _recipient, uint64 _expirationDate, bool _revocable, bytes calldata _data - ) - external - override - onlyOwnerOrApproved(commitments[_commitmentId].grantor, commitments[_commitmentId].tokenAddress) - { - require(_role == UNIQUE_ROLE, 'SftRolesRegistry: role not supported'); - require(_expirationDate > block.timestamp, 'SftRolesRegistry: expiration date must be in the future'); - _grantOrUpdateRole(_commitmentId, _role, _grantee, _expirationDate, _revocable, _data); - } - - function revokeRole( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) external override sameGrantee(_commitmentId, _role, _grantee) { - RoleAssignment storage roleAssignment = roleAssignments[_commitmentId][_role]; - Commitment storage commitment = commitments[_commitmentId]; - address caller = _findCaller(commitment.grantor, roleAssignment.grantee, commitment.tokenAddress); - if (roleAssignment.expirationDate > block.timestamp && !roleAssignment.revocable) { + ) external { + _grantERC7589Role(_lockId, _roleId, _recipient, _expirationDate, _revocable, _data); + } + + function revokeRole(uint256 _lockId, bytes32 _roleId, address _recipient) external { + TokenLock storage _tokenLock = tokenLocks[_lockId]; + Role storage _role = roles[_lockId][_roleId]; + require(_role.expirationDate > block.timestamp, 'ERC7589RolesRegistry: role does not exist'); + + // ensure caller is approved + address caller = _findCaller(_tokenLock.owner, _role.recipient, _tokenLock.tokenAddress); + if (_role.expirationDate > block.timestamp && !_role.revocable) { // if role is not expired and is not revocable, only the grantee can revoke it - require(caller == roleAssignment.grantee, 'SftRolesRegistry: role is not expired and is not revocable'); + require( + caller == _role.recipient, + 'ERC7589RolesRegistry: role is not revocable or caller is not the approved' + ); } - emit RoleRevoked(_commitmentId, _role, roleAssignment.grantee); - delete roleAssignments[_commitmentId][_role]; + + if (!_role.revocable) { + tokenLockExpirationDates[_lockId].remove(_role.expirationDate); + } + + delete roles[_lockId][_roleId]; + emit RoleRevoked(_lockId, _roleId, _recipient); } - function releaseTokens( - uint256 _commitmentId - ) external onlyOwnerOrApproved(commitments[_commitmentId].grantor, commitments[_commitmentId].tokenAddress) { - require( - roleAssignments[_commitmentId][UNIQUE_ROLE].expirationDate < block.timestamp || - roleAssignments[_commitmentId][UNIQUE_ROLE].revocable, - 'SftRolesRegistry: commitment has an active non-revocable role' - ); + function unlockTokens(uint256 _lockId) external onlyTokenLockOwnerOrApproved(_lockId) { + uint64 _headExpirationDate = tokenLockExpirationDates[_lockId].head; + require(_headExpirationDate < block.timestamp, 'ERC7589RolesRegistry: NFT is locked'); - delete roleAssignments[_commitmentId][UNIQUE_ROLE]; + address _owner = tokenLocks[_lockId].owner; + address _tokenAddress = tokenLocks[_lockId].tokenAddress; + uint256 _tokenId = tokenLocks[_lockId].tokenId; + uint256 _tokenAmount = tokenLocks[_lockId].tokenAmount; + delete tokenLocks[_lockId]; + emit TokensUnlocked(_lockId); - _transferFrom( - address(this), - commitments[_commitmentId].grantor, - commitments[_commitmentId].tokenAddress, - commitments[_commitmentId].tokenId, - commitments[_commitmentId].tokenAmount - ); + _transferFrom(address(this), _owner, _tokenAddress, _tokenId, _tokenAmount); - delete commitments[_commitmentId]; - emit TokensReleased(_commitmentId); - } + } - function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _isApproved) external override { - roleApprovals[msg.sender][_tokenAddress][_operator] = _isApproved; - emit RoleApprovalForAll(_tokenAddress, _operator, _isApproved); + function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _approved) external { + roleApprovals[msg.sender][_tokenAddress][_operator] = _approved; } - /** Optional External Functions **/ + /** ERC-7589 Lock Tokens and Grant Role Extension External Functions **/ - function commitTokensAndGrantRole( - address _grantor, + function lockTokensAndGrantRole( + address _owner, address _tokenAddress, uint256 _tokenId, uint256 _tokenAmount, - bytes32 _role, - address _grantee, + bytes32 _roleId, + address _recipient, uint64 _expirationDate, bool _revocable, bytes calldata _data - ) external override onlyOwnerOrApproved(_grantor, _tokenAddress) returns (uint256 commitmentId_) { - require(_tokenAmount > 0, 'SftRolesRegistry: tokenAmount must be greater than zero'); - require(_role == UNIQUE_ROLE, 'SftRolesRegistry: role not supported'); - require(_expirationDate > block.timestamp, 'SftRolesRegistry: expiration date must be in the future'); - commitmentId_ = _createCommitment(_grantor, _tokenAddress, _tokenId, _tokenAmount); - _grantOrUpdateRole(commitmentId_, _role, _grantee, _expirationDate, _revocable, _data); + ) external returns (uint256 lockId_) { + lockId_ = _lockTokens(_owner, _tokenAddress, _tokenId, _tokenAmount); + _grantERC7589Role(lockId_, _roleId, _recipient, _expirationDate, _revocable, _data); + } + + /** Manager External Functions **/ + + function setMarketplaceAddress(address _marketplaceAddress) external onlyManager { + marketplaceAddress = _marketplaceAddress; + } + + function setTokenAddressAllowed(address _tokenAddress, bool _isAllowed) external onlyManager { + isTokenAddressAllowed[_tokenAddress] = _isAllowed; + } + + function setManagerAddress(address _managerAddress) external onlyManager { + managerAddress = _managerAddress; } /** View Functions **/ - function grantorOf(uint256 _commitmentId) external view returns (address grantor_) { - grantor_ = commitments[_commitmentId].grantor; + function ownerOf(uint256 _lockId) external view returns (address owner_) { + return tokenLocks[_lockId].owner; } - function tokenAddressOf(uint256 _commitmentId) external view returns (address tokenAddress_) { - tokenAddress_ = commitments[_commitmentId].tokenAddress; + function tokenAddressOf(uint256 _lockId) external view returns (address tokenAddress_) { + return tokenLocks[_lockId].tokenAddress; } - function tokenIdOf(uint256 _commitmentId) external view returns (uint256 tokenId_) { - tokenId_ = commitments[_commitmentId].tokenId; + function tokenIdOf(uint256 _lockId) external view returns (uint256 tokenId_) { + return tokenLocks[_lockId].tokenId; } - function tokenAmountOf(uint256 _commitmentId) external view returns (uint256 tokenAmount_) { - tokenAmount_ = commitments[_commitmentId].tokenAmount; + function tokenAmountOf(uint256 _lockId) external view returns (uint256 tokenAmount_) { + return tokenLocks[_lockId].tokenAmount; } - function roleData( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) external view sameGrantee(_commitmentId, _role, _grantee) returns (bytes memory data_) { - return roleAssignments[_commitmentId][_role].data; + function roleData(uint256 _lockId, bytes32 _roleId) external view returns (bytes memory data_) { + if (roles[_lockId][_roleId].expirationDate > block.timestamp) { + return roles[_lockId][_roleId].data; + } + return ''; } - function roleExpirationDate( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) external view sameGrantee(_commitmentId, _role, _grantee) returns (uint64 expirationDate_) { - return roleAssignments[_commitmentId][_role].expirationDate; + function roleExpirationDate(uint256 _lockId, bytes32 _roleId) external view returns (uint64 expirationDate_) { + if (roles[_lockId][_roleId].expirationDate > block.timestamp) { + return roles[_lockId][_roleId].expirationDate; + } + return 0; } - function isRoleRevocable( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) external view sameGrantee(_commitmentId, _role, _grantee) returns (bool revocable_) { - return roleAssignments[_commitmentId][_role].revocable; + function isRoleRevocable(uint256 _lockId, bytes32 _roleId) external view returns (bool revocable_) { + if (roles[_lockId][_roleId].expirationDate > block.timestamp) { + return roles[_lockId][_roleId].revocable; + } + return false; } function isRoleApprovedForAll( address _tokenAddress, - address _grantor, + address _owner, address _operator - ) public view override returns (bool) { - return roleApprovals[_grantor][_tokenAddress][_operator]; + ) public view returns (bool isApproved_) { + return _operator == marketplaceAddress || roleApprovals[_owner][_tokenAddress][_operator]; } + /** ERC-165 View Functions **/ + function supportsInterface( bytes4 interfaceId - ) public view virtual override(ERC1155Receiver, IERC165) returns (bool) { - return - interfaceId == type(IERC7589).interfaceId || - interfaceId == type(IERC1155Receiver).interfaceId || - interfaceId == type(ICommitTokensAndGrantRoleExtension).interfaceId; + ) public view virtual override(IERC165, ERC1155Receiver) returns (bool) { + return supportedInterfaces[interfaceId]; } /** Helper Functions **/ - function _createCommitment( - address _grantor, + function _lockTokens( + address _owner, address _tokenAddress, uint256 _tokenId, uint256 _tokenAmount - ) internal returns (uint256 commitmentId_) { - commitmentId_ = ++commitmentCount; - commitments[commitmentId_] = Commitment(_grantor, _tokenAddress, _tokenId, _tokenAmount); - _transferFrom(_grantor, address(this), _tokenAddress, _tokenId, _tokenAmount); - emit TokensCommitted(_grantor, commitmentId_, _tokenAddress, _tokenId, _tokenAmount); - } - - function _grantOrUpdateRole( - uint256 _commitmentId, - bytes32 _role, - address _grantee, + ) + private + onlyAllowedTokenAddress(_tokenAddress) + onlyOwnerOrApproved(_owner, _tokenAddress) + returns (uint256 lockId_) + { + require(_tokenAmount > 0, 'ERC7589RolesRegistry: tokenAmount must be greater than zero'); + lockId_ = ++lockIdCount; + tokenLocks[lockId_] = TokenLock(_owner, _tokenAddress, _tokenId, _tokenAmount); + emit TokensLocked(_owner, lockId_, _tokenAddress, _tokenId, _tokenAmount); + _transferFrom(_owner, address(this), _tokenAddress, _tokenId, _tokenAmount); + } + + function _grantERC7589Role( + uint256 _lockId, + bytes32 _roleId, + address _recipient, uint64 _expirationDate, bool _revocable, bytes calldata _data - ) internal { - roleAssignments[_commitmentId][_role] = RoleAssignment(_grantee, _expirationDate, _revocable, _data); - emit RoleGranted(_commitmentId, _role, _grantee, _expirationDate, _revocable, _data); + ) private onlyTokenLockOwnerOrApproved(_lockId) { + require(_expirationDate > block.timestamp, 'ERC7589RolesRegistry: expirationDate must be in the future'); + + // only grant new role if previous role is expired or revocable + Role storage _currentRole = roles[_lockId][_roleId]; + require( + _currentRole.expirationDate < block.timestamp || _currentRole.revocable, + 'ERC7589RolesRegistry: role is not expired nor revocable' + ); + + // if role is not revocable + if (!_revocable) { + // add expiration date to lock list + tokenLockExpirationDates[_lockId].insert(_expirationDate); + } + + roles[_lockId][_roleId] = Role(_recipient, _expirationDate, _revocable, _data); + emit RoleGranted(_lockId, _roleId, _recipient, _expirationDate, _revocable, _data); } function _transferFrom( @@ -234,16 +296,15 @@ contract SftRolesRegistrySingleRole is IERC7589, ERC1155Holder, ICommitTokensAnd } // careful with the following edge case: - // if grantee is approved by grantor, the first one checked is returned - // if grantor is returned instead of grantee, the grantee won't be able - // to revoke the role assignment before the expiration date - function _findCaller(address _grantor, address _grantee, address _tokenAddress) internal view returns (address) { - if (_grantee == msg.sender || isRoleApprovedForAll(_tokenAddress, _grantee, msg.sender)) { - return _grantee; + // if both owner and recipient approve the sender, the recipient should be returned + // if owner is returned instead, the recipient won't be able to revoke roles + function _findCaller(address _owner, address _recipient, address _tokenAddress) internal view returns (address) { + if (_recipient == msg.sender || isRoleApprovedForAll(_tokenAddress, _recipient, msg.sender)) { + return _recipient; } - if (_grantor == msg.sender || isRoleApprovedForAll(_tokenAddress, _grantor, msg.sender)) { - return _grantor; + if (_owner == msg.sender || isRoleApprovedForAll(_tokenAddress, _owner, msg.sender)) { + return _owner; } - revert('SftRolesRegistry: sender must be approved'); + revert('ERC7589RolesRegistry: sender is not approved'); } -} +} \ No newline at end of file diff --git a/contracts/mocks/Uint64SortedLinkedListLibrary.sol b/contracts/mocks/Uint64SortedLinkedListLibrary.sol new file mode 100644 index 0000000..2104b2b --- /dev/null +++ b/contracts/mocks/Uint64SortedLinkedListLibrary.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +/// @title Uint64SortedLinkedListLibrary +/// @dev Implementation of a linked list of uint64 keys. The list is ordered in descending order, and allows duplicates. +library Uint64SortedLinkedListLibrary { + uint64 private constant EMPTY = 0; + + struct Item { + uint64 prev; + uint64 next; + uint8 count; + } + + struct List { + uint64 head; + mapping(uint64 => Item) items; + } + + /// @notice Inserts a new item into the list. + /// @dev It should maintain the descending order of the list. + /// @param _self The list to insert into. + /// @param _key The new item to be inserted. + function insert(List storage _self, uint64 _key) internal { + require(_key != EMPTY, 'Uint64SortedLinkedListLibrary: key cannot be zero'); + + // if _key already exists, only increase counter + if (_self.items[_key].count > 0) { + _self.items[_key].count++; + return; + } + + // if _key is the highest in the list, insert as head + if (_key > _self.head) { + _self.items[_key] = Item(EMPTY, _self.head, 1); + + // only update the previous head if list was not empty + if (_self.head != EMPTY) _self.items[_self.head].prev = _key; + + _self.head = _key; + return; + } + + // loop until position to insert is found + uint64 _itemKey = _self.head; + Item storage _item = _self.items[_itemKey]; + while (_key < _item.next && _item.next != EMPTY) { + _itemKey = _item.next; + _item = _self.items[_itemKey]; + } + + // if found item is tail, next is EMPTY + if (_item.next == EMPTY) { + _self.items[_key] = Item(_itemKey, EMPTY, 1); + _item.next = _key; + return; + } + + // if not tail, insert between two items + _self.items[_key] = Item(_itemKey, _item.next, 1); + _self.items[_item.next].prev = _key; + _item.next = _key; + } + + /// @notice Removes an item from the list. + /// @dev It should maintain the descending order of the list. + /// @param _self The list to remove from. + /// @param _key The item to be removed. + function remove(List storage _self, uint64 _key) internal { + Item storage _itemToUpdate = _self.items[_key]; + + // if _key does not exist, return + if (_itemToUpdate.count == 0) { + return; + } + + // if _key occurs more than once, just decrease counter + if (_itemToUpdate.count > 1) { + _itemToUpdate.count--; + return; + } + + // updating list + + // if _key is the head, update head to the next item + if (_itemToUpdate.prev == EMPTY) { + _self.head = _itemToUpdate.next; + + // only update next item if it exists (if it's not head and tail simultaneously) + if (_itemToUpdate.next != EMPTY) _self.items[_itemToUpdate.next].prev = EMPTY; + + delete _self.items[_key]; + return; + } + + // if _key is not head, but it is tail, update the previous item's next pointer to EMPTY + if (_itemToUpdate.next == EMPTY) { + _self.items[_itemToUpdate.prev].next = EMPTY; + delete _self.items[_key]; + return; + } + + // if not head nor tail, update both previous and next items + _self.items[_itemToUpdate.next].prev = _itemToUpdate.prev; + _self.items[_itemToUpdate.prev].next = _itemToUpdate.next; + delete _self.items[_key]; + } +} diff --git a/test/OriumSftMarketplace.test.ts b/test/OriumSftMarketplace.test.ts index 0943384..02d4a52 100644 --- a/test/OriumSftMarketplace.test.ts +++ b/test/OriumSftMarketplace.test.ts @@ -111,6 +111,8 @@ describe('OriumSftMarketplace', () => { rolesData: [EMPTY_BYTES], } + await rolesRegistry.setTokenAddressAllowed(rentalOffer.tokenAddress, true) + await mockERC1155.mint(lender.address, tokenId, tokenAmount, '0x') await rolesRegistry .connect(lender) @@ -146,7 +148,7 @@ describe('OriumSftMarketplace', () => { tokenId, tokenAmount, ) - .to.emit(rolesRegistry, 'TokensCommitted') + .to.emit(rolesRegistry, 'TokensLocked') .withArgs(lender.address, 1, await mockERC1155.getAddress(), tokenId, tokenAmount) }) it('Should create a rental offer if collection has a custom roles registry', async function () { @@ -179,7 +181,7 @@ describe('OriumSftMarketplace', () => { tokenId, tokenAmount, ) - .to.emit(rolesRegistry, 'TokensCommitted') + .to.emit(rolesRegistry, 'TokensLocked') .withArgs(lender.address, 1, await mockERC1155.getAddress(), tokenId, tokenAmount) }) it('Should create a rental offer with feeAmountPerSecond equal to 0 if offer is private', async function () { @@ -210,7 +212,7 @@ describe('OriumSftMarketplace', () => { tokenId, tokenAmount, ) - .to.emit(rolesRegistry, 'TokensCommitted') + .to.emit(rolesRegistry, 'TokensLocked') .withArgs(lender.address, 1, await mockERC1155.getAddress(), tokenId, tokenAmount) }) it('Should NOT create a rental offer if caller is not the lender', async () => { @@ -337,7 +339,7 @@ describe('OriumSftMarketplace', () => { rentalOffer.roles, rentalOffer.rolesData, ) - .to.not.emit(rolesRegistry, 'TokensCommitted') + .to.not.emit(rolesRegistry, 'TokensLocked') .to.not.emit(mockERC1155, 'TransferSingle') }) it("Should NOT create a rental offer if commitmentId grantor and offer lender's address are different", async () => { @@ -345,7 +347,7 @@ describe('OriumSftMarketplace', () => { await mockERC1155.connect(creator).setApprovalForAll(await rolesRegistry.getAddress(), true) await rolesRegistry .connect(creator) - .commitTokens(creator.address, rentalOffer.tokenAddress, rentalOffer.tokenId, rentalOffer.tokenAmount) + .lockTokens(creator.address, rentalOffer.tokenAddress, rentalOffer.tokenId, rentalOffer.tokenAmount) rentalOffer.commitmentId = BigInt(2) await expect(marketplace.connect(lender).createRentalOffer(rentalOffer)).to.be.revertedWith( 'OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId', @@ -355,6 +357,9 @@ describe('OriumSftMarketplace', () => { const AnotherMockERC1155 = await ethers.getContractFactory('MockERC1155') const anotherMockERC1155 = await AnotherMockERC1155.deploy() await anotherMockERC1155.waitForDeployment() + const marketplaceaddress = await anotherMockERC1155.getAddress() + await rolesRegistry.setTokenAddressAllowed(marketplaceaddress, true) + await anotherMockERC1155.mint(lender.address, tokenId, tokenAmount, '0x') await anotherMockERC1155.connect(lender).setApprovalForAll(await rolesRegistry.getAddress(), true) await marketplaceRoyalties @@ -366,7 +371,7 @@ describe('OriumSftMarketplace', () => { ) await rolesRegistry .connect(lender) - .commitTokens(lender.address, await anotherMockERC1155.getAddress(), tokenId, tokenAmount) + .lockTokens(lender.address, await anotherMockERC1155.getAddress(), tokenId, tokenAmount) rentalOffer.tokenAddress = await anotherMockERC1155.getAddress() await expect(marketplace.connect(lender).createRentalOffer(rentalOffer)).to.be.revertedWith( @@ -379,7 +384,7 @@ describe('OriumSftMarketplace', () => { await mockERC1155.connect(lender).setApprovalForAll(await rolesRegistry.getAddress(), true) await rolesRegistry .connect(lender) - .commitTokens(lender.address, rentalOffer.tokenAddress, newTokenId, rentalOffer.tokenAmount) + .lockTokens(lender.address, rentalOffer.tokenAddress, newTokenId, rentalOffer.tokenAmount) rentalOffer.commitmentId = BigInt(2) await expect(marketplace.connect(lender).createRentalOffer(rentalOffer)).to.be.revertedWith( "OriumSftMarketplace: tokenId provided does not match commitment's tokenId", @@ -592,11 +597,11 @@ describe('OriumSftMarketplace', () => { await expect(marketplace.connect(lender).cancelRentalOffer(rentalOffer)) .to.emit(marketplace, 'RentalOfferCancelled') .withArgs(rentalOffer.lender, rentalOffer.nonce) - .to.emit(rolesRegistry, 'TokensReleased') + .to.emit(rolesRegistry, 'TokensUnlocked') .withArgs(rentalOffer.commitmentId) }) it('Should cancel a rental offer if tokens was released before directly from registry', async () => { - await rolesRegistry.connect(lender).releaseTokens(rentalOffer.commitmentId) + await rolesRegistry.connect(lender).unlockTokens(rentalOffer.commitmentId) await expect(marketplace.connect(lender).cancelRentalOffer(rentalOffer)) .to.emit(marketplace, 'RentalOfferCancelled') .withArgs(rentalOffer.lender, rentalOffer.nonce) @@ -636,13 +641,13 @@ describe('OriumSftMarketplace', () => { await expect(marketplace.connect(lender).delistRentalOfferAndWithdraw(rentalOffer)) .to.emit(marketplace, 'RentalOfferCancelled') .withArgs(rentalOffer.lender, rentalOffer.nonce) - .to.emit(rolesRegistry, 'TokensReleased') + .to.emit(rolesRegistry, 'TokensUnlocked') .withArgs(rentalOffer.commitmentId) }) it('Should NOT delist a rental offer if tokens was released before directly from registry', async () => { - await rolesRegistry.connect(lender).releaseTokens(rentalOffer.commitmentId) + await rolesRegistry.connect(lender).unlockTokens(rentalOffer.commitmentId) await expect(marketplace.connect(lender).delistRentalOfferAndWithdraw(rentalOffer)).to.be.revertedWith( - 'SftRolesRegistry: account not approved', + 'ERC7589RolesRegistry: sender is not owner or approved', ) }) it('Should NOT delist a rental offer if contract is paused', async () => { @@ -680,10 +685,10 @@ describe('OriumSftMarketplace', () => { await expect(marketplace.connect(lender).delistRentalOffer(rentalOffer)) .to.emit(marketplace, 'RentalOfferCancelled') .withArgs(rentalOffer.lender, rentalOffer.nonce) - .to.not.emit(rolesRegistry, 'TokensReleased') + .to.not.emit(rolesRegistry, 'TokensUnlocked') }) it('Should delist a rental offer if tokens was released before directly from registry', async () => { - await rolesRegistry.connect(lender).releaseTokens(rentalOffer.commitmentId) + await rolesRegistry.connect(lender).unlockTokens(rentalOffer.commitmentId) await expect(marketplace.connect(lender).delistRentalOffer(rentalOffer)) .to.emit(marketplace, 'RentalOfferCancelled') .withArgs(rentalOffer.lender, rentalOffer.nonce) @@ -724,7 +729,7 @@ describe('OriumSftMarketplace', () => { await expect( marketplace.connect(lender).batchReleaseTokens([rentalOffer.tokenAddress], [rentalOffer.commitmentId]), ) - .to.emit(rolesRegistry, 'TokensReleased') + .to.emit(rolesRegistry, 'TokensUnlocked') .withArgs(rentalOffer.commitmentId) }) it('Should NOT release tokens if contract is paused', async () => { @@ -808,7 +813,7 @@ describe('OriumSftMarketplace', () => { .connect(borrower) .revokeRole(rentalOffer.commitmentId, rentalOffer.roles[0], borrower.address) await expect(marketplace.connect(borrower).endRental(rentalOffer)).to.be.revertedWith( - 'SftRolesRegistry: grantee mismatch', + 'ERC7589RolesRegistry: role does not exist', ) }) it('Should NOT end rental twice', async () => { @@ -824,7 +829,7 @@ describe('OriumSftMarketplace', () => { await expect(marketplace.connect(lender).cancelRentalOffer(rentalOffer)) .to.emit(marketplace, 'RentalOfferCancelled') .withArgs(rentalOffer.lender, rentalOffer.nonce) - .to.not.emit(rolesRegistry, 'TokensReleased') + .to.not.emit(rolesRegistry, 'TokensUnlocked') }) }) @@ -837,7 +842,7 @@ describe('OriumSftMarketplace', () => { .connect(lender) .batchReleaseTokens([rentalOffer.tokenAddress], [rentalOffer.commitmentId]), ) - .to.emit(rolesRegistry, 'TokensReleased') + .to.emit(rolesRegistry, 'TokensUnlocked') .withArgs(rentalOffer.commitmentId) }) it('Should NOT release tokens if rental is active', async function () { @@ -845,7 +850,7 @@ describe('OriumSftMarketplace', () => { marketplace .connect(lender) .batchReleaseTokens([rentalOffer.tokenAddress], [rentalOffer.commitmentId]), - ).to.be.revertedWith('SftRolesRegistry: commitment has an active non-revocable role') + ).to.be.revertedWith('ERC7589RolesRegistry: NFT is locked') }) }) }) @@ -876,8 +881,10 @@ describe('OriumSftMarketplace', () => { await mockERC1155.connect(lender).setApprovalForAll(await rolesRegistry.getAddress(), true) }) it('Should commit tokens and grant role', async () => { + await rolesRegistry.setTokenAddressAllowed(commitAndGrantRoleParams[0].tokenAddress, true) + await expect(marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams)) - .to.emit(rolesRegistry, 'TokensCommitted') + .to.emit(rolesRegistry, 'TokensLocked') .withArgs(lender.address, 1, await mockERC1155.getAddress(), tokenId, tokenAmount) .to.emit(rolesRegistry, 'RoleGranted') .withArgs( @@ -890,10 +897,12 @@ describe('OriumSftMarketplace', () => { ) }) it('Should only grant role when a commitmentId is passed', async () => { + await rolesRegistry.setTokenAddressAllowed(commitAndGrantRoleParams[0].tokenAddress, true) + commitAndGrantRoleParams[0].commitmentId = BigInt(1) await rolesRegistry .connect(lender) - .commitTokens(lender.address, await mockERC1155.getAddress(), tokenId, tokenAmount) + .lockTokens(lender.address, await mockERC1155.getAddress(), tokenId, tokenAmount) await expect(marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams)) .to.emit(rolesRegistry, 'RoleGranted') .withArgs( @@ -912,40 +921,44 @@ describe('OriumSftMarketplace', () => { ).to.be.revertedWith('Pausable: paused') }) it('Should NOT commit tokens and grant role if caller is not the grantor of the commitmentId', async () => { + await rolesRegistry.setTokenAddressAllowed(commitAndGrantRoleParams[0].tokenAddress, true) commitAndGrantRoleParams[0].commitmentId = BigInt(1) await rolesRegistry .connect(lender) - .commitTokens(lender.address, await mockERC1155.getAddress(), tokenId, tokenAmount) + .lockTokens(lender.address, await mockERC1155.getAddress(), tokenId, tokenAmount) await expect( marketplace.connect(borrower).batchCommitTokensAndGrantRole(commitAndGrantRoleParams), ).to.revertedWith('OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId') }) it('Should NOT commit tokens and grant role if tokenAddress does not match the commitment', async () => { + await rolesRegistry.setTokenAddressAllowed(commitAndGrantRoleParams[0].tokenAddress, true) commitAndGrantRoleParams[0].commitmentId = BigInt(1) commitAndGrantRoleParams[0].tokenAddress = AddressZero await rolesRegistry .connect(lender) - .commitTokens(lender.address, await mockERC1155.getAddress(), tokenId, tokenAmount) + .lockTokens(lender.address, await mockERC1155.getAddress(), tokenId, tokenAmount) await expect( marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams), ).to.revertedWith("OriumSftMarketplace: tokenAddress provided does not match commitment's tokenAddress") }) it('Should NOT commit tokens and grant role if tokenId does not match the commitment', async () => { + await rolesRegistry.setTokenAddressAllowed(commitAndGrantRoleParams[0].tokenAddress, true) commitAndGrantRoleParams[0].commitmentId = BigInt(1) commitAndGrantRoleParams[0].tokenId = 0 await rolesRegistry .connect(lender) - .commitTokens(lender.address, await mockERC1155.getAddress(), tokenId, tokenAmount) + .lockTokens(lender.address, await mockERC1155.getAddress(), tokenId, tokenAmount) await expect( marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams), ).to.revertedWith("OriumSftMarketplace: tokenId provided does not match commitment's tokenId") }) it('Should NOT commit tokens and grant role if tokenAmount does not match the commitment', async () => { + await rolesRegistry.setTokenAddressAllowed(commitAndGrantRoleParams[0].tokenAddress, true) commitAndGrantRoleParams[0].commitmentId = BigInt(1) commitAndGrantRoleParams[0].tokenAmount = BigInt(0) await rolesRegistry .connect(lender) - .commitTokens(lender.address, await mockERC1155.getAddress(), tokenId, tokenAmount) + .lockTokens(lender.address, await mockERC1155.getAddress(), tokenId, tokenAmount) await expect( marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams), ).to.revertedWith("OriumSftMarketplace: tokenAmount provided does not match commitment's tokenAmount") @@ -969,6 +982,7 @@ describe('OriumSftMarketplace', () => { data: EMPTY_BYTES, }, ] + await rolesRegistry.setTokenAddressAllowed(commitAndGrantRoleParams[0].tokenAddress, true) await rolesRegistry .connect(lender) .setRoleApprovalForAll(await mockERC1155.getAddress(), await marketplace.getAddress(), true) diff --git a/test/fixtures/OriumMarketplaceRoyaltiesFixture.ts b/test/fixtures/OriumMarketplaceRoyaltiesFixture.ts index 589fe73..523a42f 100644 --- a/test/fixtures/OriumMarketplaceRoyaltiesFixture.ts +++ b/test/fixtures/OriumMarketplaceRoyaltiesFixture.ts @@ -11,8 +11,14 @@ export async function deployMarketplaceRoyaltiesContracts() { const nftRolesRegistry: IERC7432 = await ethers.getContractAt('IERC7432', RolesRegistryAddress) + const MockERC1155Factory = await ethers.getContractFactory('MockERC1155') + const mockERC1155: MockERC1155 = await MockERC1155Factory.deploy() + await mockERC1155.waitForDeployment() + + const marketplaceaddress = await mockERC1155.getAddress() + const SftRolesRegistryFactory = await ethers.getContractFactory('SftRolesRegistrySingleRole') - const sftRolesRegistry: SftRolesRegistrySingleRole = await SftRolesRegistryFactory.deploy() + const sftRolesRegistry: SftRolesRegistrySingleRole = await SftRolesRegistryFactory.deploy(marketplaceaddress) await sftRolesRegistry.waitForDeployment() const MarketplaceRoyaltiesFactory = await ethers.getContractFactory('OriumMarketplaceRoyalties') @@ -29,10 +35,6 @@ export async function deployMarketplaceRoyaltiesContracts() { await marketplaceRoyaltiesProxy.getAddress(), ) - const MockERC1155Factory = await ethers.getContractFactory('MockERC1155') - const mockERC1155: MockERC1155 = await MockERC1155Factory.deploy() - await mockERC1155.waitForDeployment() - const MockERC20Factory = await ethers.getContractFactory('MockERC20') const mockERC20: MockERC20 = await MockERC20Factory.deploy() await mockERC20.waitForDeployment() diff --git a/test/fixtures/OriumSftMarketplaceFixture.ts b/test/fixtures/OriumSftMarketplaceFixture.ts index 57f5f8b..eeb881b 100644 --- a/test/fixtures/OriumSftMarketplaceFixture.ts +++ b/test/fixtures/OriumSftMarketplaceFixture.ts @@ -1,6 +1,11 @@ import { ethers, upgrades } from 'hardhat' import { AddressZero, THREE_MONTHS } from '../../utils/constants' -import { OriumMarketplaceRoyalties, OriumSftMarketplace, SftRolesRegistrySingleRole } from '../../typechain-types' +import { + OriumMarketplaceRoyalties, + OriumSftMarketplace, + SftRolesRegistrySingleRole, + MockERC1155, +} from '../../typechain-types' /** * @dev deployer, operator needs to be the first accounts in the hardhat ethers.getSigners() @@ -10,8 +15,14 @@ import { OriumMarketplaceRoyalties, OriumSftMarketplace, SftRolesRegistrySingleR export async function deploySftMarketplaceContracts() { const [, operator] = await ethers.getSigners() + const MockERC1155Factory = await ethers.getContractFactory('MockERC1155') + const mockERC1155: MockERC1155 = await MockERC1155Factory.deploy() + await mockERC1155.waitForDeployment() + + const marketplaceaddress = await mockERC1155.getAddress() + const RolesRegistryFactory = await ethers.getContractFactory('SftRolesRegistrySingleRole') - const rolesRegistry: SftRolesRegistrySingleRole = await RolesRegistryFactory.deploy() + const rolesRegistry: SftRolesRegistrySingleRole = await RolesRegistryFactory.deploy(marketplaceaddress) await rolesRegistry.waitForDeployment() const MarketplaceRoyaltiesFactory = await ethers.getContractFactory('OriumMarketplaceRoyalties') @@ -49,9 +60,9 @@ export async function deploySftMarketplaceContracts() { await marketplaceProxy.getAddress(), ) - const MockERC1155Factory = await ethers.getContractFactory('MockERC1155') - const mockERC1155 = await MockERC1155Factory.deploy() - await mockERC1155.waitForDeployment() + // const MockERC1155Factory = await ethers.getContractFactory('MockERC1155') + // const mockERC1155 = await MockERC1155Factory.deploy() + // await mockERC1155.waitForDeployment() const secondMockERC1155 = await MockERC1155Factory.deploy() await secondMockERC1155.waitForDeployment() From 17cd2f848a2e6e79e9974f75b1ef3d0709b6f9f7 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Thu, 4 Jul 2024 18:34:24 -0300 Subject: [PATCH 03/13] feat: update createRentalOffer and batch to verify if is weareable and call legacy interface --- contracts/OriumSftMarketplace.sol | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/contracts/OriumSftMarketplace.sol b/contracts/OriumSftMarketplace.sol index eebcdd6..5440d15 100644 --- a/contracts/OriumSftMarketplace.sol +++ b/contracts/OriumSftMarketplace.sol @@ -7,6 +7,7 @@ import { Initializable } from '@openzeppelin/contracts-upgradeable/proxy/utils/I import { PausableUpgradeable } from '@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol'; import { IERC1155 } from '@openzeppelin/contracts/token/ERC1155/IERC1155.sol'; import { IERC7589 } from './interfaces/IERC7589.sol'; +import { IERC7589Legacy } from './interfaces/IERC7589Legacy.sol'; import { LibOriumSftMarketplace, RentalOffer, CommitAndGrantRoleParams } from './libraries/LibOriumSftMarketplace.sol'; import { IOriumMarketplaceRoyalties } from './interfaces/IOriumMarketplaceRoyalties.sol'; @@ -21,6 +22,9 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra /// @dev oriumMarketplaceRoyalties stores the collection royalties and fees address public oriumMarketplaceRoyalties; + /// @dev Wearable address + address public constant wearableAddress = 0x58de9AaBCaeEC0f69883C94318810ad79Cc6a44f; + /// @dev hashedOffer => bool mapping(bytes32 => bool) public isCreated; @@ -128,12 +132,21 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra _validateCreateRentalOffer(_offer, _rolesRegistryAddress); if (_offer.commitmentId == 0) { - _offer.commitmentId = IERC7589(_rolesRegistryAddress).lockTokens( + if(_offer.tokenAddress == wearableAddress ){ + _offer.commitmentId = IERC7589Legacy(_rolesRegistryAddress).commitTokens( _offer.lender, _offer.tokenAddress, _offer.tokenId, _offer.tokenAmount ); + }else { + _offer.commitmentId = IERC7589(_rolesRegistryAddress).lockTokens( + _offer.lender, + _offer.tokenAddress, + _offer.tokenId, + _offer.tokenAmount + ); + } } nonceDeadline[msg.sender][_offer.nonce] = _offer.deadline; @@ -301,15 +314,26 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra _params[i].tokenAddress ); + IERC7589Legacy _rolesRegistryLegacy = IERC7589Legacy(_rolesRegistryAddress); IERC7589 _rolesRegistry = IERC7589(_rolesRegistryAddress); + uint256 _validCommitmentId = _params[i].commitmentId; if (_params[i].commitmentId == 0) { + if(_params[i].tokenAddress == wearableAddress) { + _validCommitmentId = _rolesRegistryLegacy.commitTokens( + msg.sender, + _params[i].tokenAddress, + _params[i].tokenId, + _params[i].tokenAmount + ); + } else { _validCommitmentId = _rolesRegistry.lockTokens( msg.sender, _params[i].tokenAddress, _params[i].tokenId, _params[i].tokenAmount ); + } } else { // if the user provided a valid commitmentId, it's necessary to verify if they actually own it, // and if all other parameters passed match the commitmentId From 62a36109c6b86304a2b44dbd3697413699556791 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Mon, 8 Jul 2024 00:47:37 -0300 Subject: [PATCH 04/13] fix: updating tests to to conver batchCommitTokensAndGrantRole --- contracts/OriumSftMarketplace.sol | 3 +- ...ommitTokensAndGrantRoleExtensionLegacy.sol | 28 ++ contracts/mocks/ISftRolesRegistryLegacy.sol | 182 +++++++++++++ .../SftRolesRegistrySingleRoleLegacy.sol | 250 ++++++++++++++++++ test/OriumSftMarketplace.test.ts | 116 +++++++- test/fixtures/OriumSftMarketplaceFixture.ts | 22 +- 6 files changed, 591 insertions(+), 10 deletions(-) create mode 100644 contracts/mocks/ICommitTokensAndGrantRoleExtensionLegacy.sol create mode 100644 contracts/mocks/ISftRolesRegistryLegacy.sol create mode 100644 contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol diff --git a/contracts/OriumSftMarketplace.sol b/contracts/OriumSftMarketplace.sol index 5440d15..639f2aa 100644 --- a/contracts/OriumSftMarketplace.sol +++ b/contracts/OriumSftMarketplace.sol @@ -314,13 +314,12 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra _params[i].tokenAddress ); - IERC7589Legacy _rolesRegistryLegacy = IERC7589Legacy(_rolesRegistryAddress); IERC7589 _rolesRegistry = IERC7589(_rolesRegistryAddress); uint256 _validCommitmentId = _params[i].commitmentId; if (_params[i].commitmentId == 0) { if(_params[i].tokenAddress == wearableAddress) { - _validCommitmentId = _rolesRegistryLegacy.commitTokens( + _validCommitmentId = IERC7589Legacy(_rolesRegistryAddress).commitTokens( msg.sender, _params[i].tokenAddress, _params[i].tokenId, diff --git a/contracts/mocks/ICommitTokensAndGrantRoleExtensionLegacy.sol b/contracts/mocks/ICommitTokensAndGrantRoleExtensionLegacy.sol new file mode 100644 index 0000000..d62c2e0 --- /dev/null +++ b/contracts/mocks/ICommitTokensAndGrantRoleExtensionLegacy.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +interface ICommitTokensAndGrantRoleExtensionLegacy { + /// @notice Commits tokens and grant role in a single transaction. + /// @param _grantor The owner of the SFTs. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _tokenAmount The token amount. + /// @param _role The role identifier. + /// @param _grantee The recipient 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. + /// @return commitmentId_ The identifier of the commitment created. + function commitTokensAndGrantRole( + address _grantor, + address _tokenAddress, + uint256 _tokenId, + uint256 _tokenAmount, + bytes32 _role, + address _grantee, + uint64 _expirationDate, + bool _revocable, + bytes calldata _data + ) external returns (uint256 commitmentId_); +} \ No newline at end of file diff --git a/contracts/mocks/ISftRolesRegistryLegacy.sol b/contracts/mocks/ISftRolesRegistryLegacy.sol new file mode 100644 index 0000000..a52f060 --- /dev/null +++ b/contracts/mocks/ISftRolesRegistryLegacy.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +import { IERC165 } from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; + +interface ISftRolesRegistryLegacy is IERC165 { + struct RoleAssignment { + address grantee; + uint64 expirationDate; + bool revocable; + bytes data; + } + + struct Commitment { + address grantor; + address tokenAddress; + uint256 tokenId; + uint256 tokenAmount; + } + + /** Events **/ + + /// @notice Emitted when tokens are committed (deposited or frozen). + /// @param _grantor The owner of the SFTs. + /// @param _commitmentId The identifier of the commitment created. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _tokenAmount The token amount. + event TokensCommitted( + address indexed _grantor, + uint256 indexed _commitmentId, + address indexed _tokenAddress, + uint256 _tokenId, + uint256 _tokenAmount + ); + + /// @notice Emitted when a role is granted. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient 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( + uint256 indexed _commitmentId, + bytes32 indexed _role, + address indexed _grantee, + uint64 _expirationDate, + bool _revocable, + bytes _data + ); + + /// @notice Emitted when a role is revoked. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient of the role revocation. + event RoleRevoked(uint256 indexed _commitmentId, bytes32 indexed _role, address indexed _grantee); + + /// @notice Emitted when a user releases tokens from a commitment. + /// @param _commitmentId The commitment identifier. + event TokensReleased(uint256 indexed _commitmentId); + + /// @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); + + /** External Functions **/ + + /// @notice Commits tokens (deposits on a contract or freezes balance). + /// @param _grantor The owner of the SFTs. + /// @param _tokenAddress The token address. + /// @param _tokenId The token identifier. + /// @param _tokenAmount The token amount. + /// @return commitmentId_ The unique identifier of the commitment created. + function commitTokens( + address _grantor, + address _tokenAddress, + uint256 _tokenId, + uint256 _tokenAmount + ) external returns (uint256 commitmentId_); + + /// @notice Grants a role to `_grantee`. + /// @param _commitmentId The identifier of the commitment. + /// @param _role The role identifier. + /// @param _grantee The recipient 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. + function grantRole( + uint256 _commitmentId, + bytes32 _role, + address _grantee, + uint64 _expirationDate, + bool _revocable, + bytes calldata _data + ) external; + + /// @notice Revokes a role. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient of the role revocation. + function revokeRole(uint256 _commitmentId, bytes32 _role, address _grantee) external; + + /// @notice Releases tokens back to grantor. + /// @param _commitmentId The commitment identifier. + function releaseTokens(uint256 _commitmentId) external; + + /// @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. + function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _approved) external; + + /** View Functions **/ + + /// @notice Returns the owner of the commitment (grantor). + /// @param _commitmentId The commitment identifier. + /// @return grantor_ The commitment owner. + function grantorOf(uint256 _commitmentId) external view returns (address grantor_); + + /// @notice Returns the address of the token committed. + /// @param _commitmentId The commitment identifier. + /// @return tokenAddress_ The token address. + function tokenAddressOf(uint256 _commitmentId) external view returns (address tokenAddress_); + + /// @notice Returns the identifier of the token committed. + /// @param _commitmentId The commitment identifier. + /// @return tokenId_ The token identifier. + function tokenIdOf(uint256 _commitmentId) external view returns (uint256 tokenId_); + + /// @notice Returns the amount of tokens committed. + /// @param _commitmentId The commitment identifier. + /// @return tokenAmount_ The token amount. + function tokenAmountOf(uint256 _commitmentId) external view returns (uint256 tokenAmount_); + + /// @notice Returns the custom data of a role assignment. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient the role. + /// @return data_ The custom data. + function roleData( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) external view returns (bytes memory data_); + + /// @notice Returns the expiration date of a role assignment. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient the role. + /// @return expirationDate_ The expiration date. + function roleExpirationDate( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) external view returns (uint64 expirationDate_); + + /// @notice Returns the expiration date of a role assignment. + /// @param _commitmentId The commitment identifier. + /// @param _role The role identifier. + /// @param _grantee The recipient the role. + /// @return revocable_ Whether the role is revocable or not. + function isRoleRevocable( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) external view returns (bool revocable_); + + /// @notice Checks if the grantor approved the operator for all SFTs. + /// @param _tokenAddress The token address. + /// @param _grantor The user that approved the operator. + /// @param _operator The user that can grant and revoke roles. + /// @return isApproved_ Whether the operator is approved or not. + function isRoleApprovedForAll( + address _tokenAddress, + address _grantor, + address _operator + ) external view returns (bool isApproved_); +} \ No newline at end of file diff --git a/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol b/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol new file mode 100644 index 0000000..f956972 --- /dev/null +++ b/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +import { ISftRolesRegistryLegacy } from './ISftRolesRegistryLegacy.sol'; +import { ICommitTokensAndGrantRoleExtensionLegacy } from './ICommitTokensAndGrantRoleExtensionLegacy.sol'; +import { IERC165 } from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; +import { IERC1155 } from '@openzeppelin/contracts/token/ERC1155/IERC1155.sol'; +import { IERC1155Receiver } from '@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol'; + +import { ERC1155Holder, ERC1155Receiver } from '@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol'; +import { ERC165Checker } from '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol'; + +// Semi-fungible token (SFT) registry with only one role (UNIQUE_ROLE) +contract SftRolesRegistrySingleRoleLegacy is ISftRolesRegistryLegacy, ERC1155Holder, ICommitTokensAndGrantRoleExtensionLegacy { + bytes32 public constant UNIQUE_ROLE = keccak256('UNIQUE_ROLE'); + + uint256 public commitmentCount; + + // grantor => tokenAddress => operator => isApproved + mapping(address => mapping(address => mapping(address => bool))) public roleApprovals; + + // commitmentId => Commitment + mapping(uint256 => Commitment) public commitments; + + // commitmentId => role => RoleAssignment + mapping(uint256 => mapping(bytes32 => RoleAssignment)) internal roleAssignments; + + modifier onlyOwnerOrApproved(address _account, address _tokenAddress) { + require( + _account == msg.sender || isRoleApprovedForAll(_tokenAddress, _account, msg.sender), + 'SftRolesRegistry: account not approved' + ); + _; + } + + modifier sameGrantee( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) { + require( + _grantee != address(0) && _grantee == roleAssignments[_commitmentId][_role].grantee, + 'SftRolesRegistry: grantee mismatch' + ); + _; + } + + /** External Functions **/ + + function commitTokens( + address _grantor, + address _tokenAddress, + uint256 _tokenId, + uint256 _tokenAmount + ) external override onlyOwnerOrApproved(_grantor, _tokenAddress) returns (uint256 commitmentId_) { + require(_tokenAmount > 0, 'SftRolesRegistry: tokenAmount must be greater than zero'); + commitmentId_ = _createCommitment(_grantor, _tokenAddress, _tokenId, _tokenAmount); + } + + function grantRole( + uint256 _commitmentId, + bytes32 _role, + address _grantee, + uint64 _expirationDate, + bool _revocable, + bytes calldata _data + ) + external + override + onlyOwnerOrApproved(commitments[_commitmentId].grantor, commitments[_commitmentId].tokenAddress) + { + require(_role == UNIQUE_ROLE, 'SftRolesRegistry: role not supported'); + require(_expirationDate > block.timestamp, 'SftRolesRegistry: expiration date must be in the future'); + _grantOrUpdateRole(_commitmentId, _role, _grantee, _expirationDate, _revocable, _data); + } + + function revokeRole( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) external override sameGrantee(_commitmentId, _role, _grantee) { + RoleAssignment storage roleAssignment = roleAssignments[_commitmentId][_role]; + Commitment storage commitment = commitments[_commitmentId]; + address caller = _findCaller(commitment.grantor, roleAssignment.grantee, commitment.tokenAddress); + if (roleAssignment.expirationDate > block.timestamp && !roleAssignment.revocable) { + // if role is not expired and is not revocable, only the grantee can revoke it + require(caller == roleAssignment.grantee, 'SftRolesRegistry: role is not expired and is not revocable'); + } + emit RoleRevoked(_commitmentId, _role, roleAssignment.grantee); + delete roleAssignments[_commitmentId][_role]; + } + + function releaseTokens( + uint256 _commitmentId + ) external onlyOwnerOrApproved(commitments[_commitmentId].grantor, commitments[_commitmentId].tokenAddress) { + require( + roleAssignments[_commitmentId][UNIQUE_ROLE].expirationDate < block.timestamp || + roleAssignments[_commitmentId][UNIQUE_ROLE].revocable, + 'SftRolesRegistry: commitment has an active non-revocable role' + ); + + delete roleAssignments[_commitmentId][UNIQUE_ROLE]; + + _transferFrom( + address(this), + commitments[_commitmentId].grantor, + commitments[_commitmentId].tokenAddress, + commitments[_commitmentId].tokenId, + commitments[_commitmentId].tokenAmount + ); + + delete commitments[_commitmentId]; + emit TokensReleased(_commitmentId); + } + + function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _isApproved) external override { + roleApprovals[msg.sender][_tokenAddress][_operator] = _isApproved; + emit RoleApprovalForAll(_tokenAddress, _operator, _isApproved); + } + + /** Optional External Functions **/ + + function commitTokensAndGrantRole( + address _grantor, + address _tokenAddress, + uint256 _tokenId, + uint256 _tokenAmount, + bytes32 _role, + address _grantee, + uint64 _expirationDate, + bool _revocable, + bytes calldata _data + ) external override onlyOwnerOrApproved(_grantor, _tokenAddress) returns (uint256 commitmentId_) { + require(_tokenAmount > 0, 'SftRolesRegistry: tokenAmount must be greater than zero'); + require(_role == UNIQUE_ROLE, 'SftRolesRegistry: role not supported'); + require(_expirationDate > block.timestamp, 'SftRolesRegistry: expiration date must be in the future'); + commitmentId_ = _createCommitment(_grantor, _tokenAddress, _tokenId, _tokenAmount); + _grantOrUpdateRole(commitmentId_, _role, _grantee, _expirationDate, _revocable, _data); + } + + /** View Functions **/ + + function grantorOf(uint256 _commitmentId) external view returns (address grantor_) { + grantor_ = commitments[_commitmentId].grantor; + } + + function tokenAddressOf(uint256 _commitmentId) external view returns (address tokenAddress_) { + tokenAddress_ = commitments[_commitmentId].tokenAddress; + } + + function tokenIdOf(uint256 _commitmentId) external view returns (uint256 tokenId_) { + tokenId_ = commitments[_commitmentId].tokenId; + } + + function tokenAmountOf(uint256 _commitmentId) external view returns (uint256 tokenAmount_) { + tokenAmount_ = commitments[_commitmentId].tokenAmount; + } + + function roleData( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) external view sameGrantee(_commitmentId, _role, _grantee) returns (bytes memory data_) { + return roleAssignments[_commitmentId][_role].data; + } + + function roleExpirationDate( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) external view sameGrantee(_commitmentId, _role, _grantee) returns (uint64 expirationDate_) { + return roleAssignments[_commitmentId][_role].expirationDate; + } + + function isRoleRevocable( + uint256 _commitmentId, + bytes32 _role, + address _grantee + ) external view sameGrantee(_commitmentId, _role, _grantee) returns (bool revocable_) { + return roleAssignments[_commitmentId][_role].revocable; + } + + function isRoleApprovedForAll( + address _tokenAddress, + address _grantor, + address _operator + ) public view override returns (bool) { + return roleApprovals[_grantor][_tokenAddress][_operator]; + } + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC1155Receiver, IERC165) returns (bool) { + return + interfaceId == type(ISftRolesRegistryLegacy).interfaceId || + interfaceId == type(IERC1155Receiver).interfaceId || + interfaceId == type(ICommitTokensAndGrantRoleExtensionLegacy).interfaceId; + } + + /** Helper Functions **/ + + function _createCommitment( + address _grantor, + address _tokenAddress, + uint256 _tokenId, + uint256 _tokenAmount + ) internal returns (uint256 commitmentId_) { + commitmentId_ = ++commitmentCount; + commitments[commitmentId_] = Commitment(_grantor, _tokenAddress, _tokenId, _tokenAmount); + _transferFrom(_grantor, address(this), _tokenAddress, _tokenId, _tokenAmount); + emit TokensCommitted(_grantor, commitmentId_, _tokenAddress, _tokenId, _tokenAmount); + } + + function _grantOrUpdateRole( + uint256 _commitmentId, + bytes32 _role, + address _grantee, + uint64 _expirationDate, + bool _revocable, + bytes calldata _data + ) internal { + roleAssignments[_commitmentId][_role] = RoleAssignment(_grantee, _expirationDate, _revocable, _data); + emit RoleGranted(_commitmentId, _role, _grantee, _expirationDate, _revocable, _data); + } + + function _transferFrom( + address _from, + address _to, + address _tokenAddress, + uint256 _tokenId, + uint256 _tokenAmount + ) internal { + IERC1155(_tokenAddress).safeTransferFrom(_from, _to, _tokenId, _tokenAmount, ''); + } + + // careful with the following edge case: + // if grantee is approved by grantor, the first one checked is returned + // if grantor is returned instead of grantee, the grantee won't be able + // to revoke the role assignment before the expiration date + function _findCaller(address _grantor, address _grantee, address _tokenAddress) internal view returns (address) { + if (_grantee == msg.sender || isRoleApprovedForAll(_tokenAddress, _grantee, msg.sender)) { + return _grantee; + } + if (_grantor == msg.sender || isRoleApprovedForAll(_tokenAddress, _grantor, msg.sender)) { + return _grantor; + } + revert('SftRolesRegistry: sender must be approved'); + } +} \ No newline at end of file diff --git a/test/OriumSftMarketplace.test.ts b/test/OriumSftMarketplace.test.ts index 02d4a52..769624c 100644 --- a/test/OriumSftMarketplace.test.ts +++ b/test/OriumSftMarketplace.test.ts @@ -14,14 +14,17 @@ import { OriumMarketplaceRoyalties, OriumSftMarketplace, SftRolesRegistrySingleRole, + SftRolesRegistrySingleRoleLegacy, } from '../typechain-types' describe('OriumSftMarketplace', () => { let marketplace: OriumSftMarketplace let marketplaceRoyalties: OriumMarketplaceRoyalties let rolesRegistry: SftRolesRegistrySingleRole + let SftRolesRegistrySingleRoleLegacy: SftRolesRegistrySingleRoleLegacy let mockERC1155: MockERC1155 let secondMockERC1155: MockERC1155 + let wearableToken: MockERC1155 let mockERC20: MockERC20 // We are disabling this rule because hardhat uses first account as deployer by default, and we are separating deployer and operator @@ -46,7 +49,7 @@ describe('OriumSftMarketplace', () => { beforeEach(async () => { // we are disabling this rule so ; may not be added automatically by prettier at the beginning of the line // prettier-ignore - [marketplace, marketplaceRoyalties, rolesRegistry, mockERC1155, mockERC20, secondMockERC1155] = await loadFixture(deploySftMarketplaceContracts) + [marketplace, marketplaceRoyalties, rolesRegistry, mockERC1155, mockERC20, secondMockERC1155, wearableToken, SftRolesRegistrySingleRoleLegacy] = await loadFixture(deploySftMarketplaceContracts) }) describe('Main Functions', async () => { @@ -54,6 +57,7 @@ describe('OriumSftMarketplace', () => { const duration = ONE_HOUR const tokenId = 1 const tokenAmount = BigInt(2) + const wearableAddress = '0x58de9AaBCaeEC0f69883C94318810ad79Cc6a44f' beforeEach(async () => { await mockERC1155.mint(lender.address, tokenId, tokenAmount, '0x') @@ -61,13 +65,24 @@ describe('OriumSftMarketplace', () => { await marketplaceRoyalties .connect(operator) .setTrustedFeeTokenForToken( - [await mockERC1155.getAddress(), await secondMockERC1155.getAddress()], - [await mockERC20.getAddress(), await mockERC20.getAddress()], - [true, true], + [await mockERC1155.getAddress(), await secondMockERC1155.getAddress(), wearableAddress], + [await mockERC20.getAddress(), await mockERC20.getAddress(), await mockERC20.getAddress()], + [true, true, true], ) await marketplaceRoyalties .connect(operator) .setRolesRegistry(await secondMockERC1155.getAddress(), await rolesRegistry.getAddress()) + + // Manually set its address to wearableAddress by redeploying the contract at that address + await ethers.provider.send('hardhat_setCode', [ + wearableAddress, + await ethers.provider.getCode(wearableToken.getAddress()), + ]) + + wearableToken = await ethers.getContractAt('MockERC1155', wearableAddress) + + await wearableToken.mint(lender.address, tokenId, tokenAmount, '0x') + await wearableToken.connect(lender).setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) }) describe('Rental Offers', async () => { @@ -118,6 +133,15 @@ describe('OriumSftMarketplace', () => { .connect(lender) .setRoleApprovalForAll(await mockERC1155.getAddress(), await marketplace.getAddress(), true) await mockERC1155.connect(lender).setApprovalForAll(await rolesRegistry.getAddress(), true) + + await SftRolesRegistrySingleRoleLegacy.connect(lender).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + await wearableToken + .connect(lender) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) }) describe('When Rental Offer is not created', async () => { describe('Create Rental Offer', async () => { @@ -215,6 +239,59 @@ describe('OriumSftMarketplace', () => { .to.emit(rolesRegistry, 'TokensLocked') .withArgs(lender.address, 1, await mockERC1155.getAddress(), tokenId, tokenAmount) }) + it('Should use IERC7589Legacy if tokenAddress matches wearableAddress', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry( + await wearableToken.getAddress(), + await SftRolesRegistrySingleRoleLegacy.getAddress(), + ) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + const rentalOfferLegacy = { + nonce: `0x${randomBytes(32).toString('hex')}`, + commitmentId: BigInt(0), + lender: lender.address, + borrower: AddressZero, + tokenAddress: wearableAddress, + tokenId, + tokenAmount, + feeTokenAddress: await mockERC20.getAddress(), + feeAmountPerSecond: toWei('0.0000001'), + deadline: Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY, + minDuration: 1, + roles: [UNIQUE_ROLE], + rolesData: [EMPTY_BYTES], + } + + await expect(marketplace.connect(lender).createRentalOffer(rentalOfferLegacy)) + .to.emit(marketplace, 'RentalOfferCreated') + .withArgs( + rentalOfferLegacy.nonce, + rentalOfferLegacy.tokenAddress, + rentalOfferLegacy.tokenId, + rentalOfferLegacy.tokenAmount, + 1, + rentalOfferLegacy.lender, + rentalOfferLegacy.borrower, + rentalOfferLegacy.feeTokenAddress, + rentalOfferLegacy.feeAmountPerSecond, + rentalOfferLegacy.deadline, + rentalOfferLegacy.minDuration, + rentalOfferLegacy.roles, + rentalOfferLegacy.rolesData, + ) + .to.emit(wearableToken, 'TransferSingle') + .withArgs( + await SftRolesRegistrySingleRoleLegacy.getAddress(), + lender.address, + await SftRolesRegistrySingleRoleLegacy.getAddress(), + tokenId, + tokenAmount, + ) + .to.emit(SftRolesRegistrySingleRoleLegacy, 'TokensCommitted') + .withArgs(lender.address, 1, wearableToken.getAddress(), tokenId, tokenAmount) + }) it('Should NOT create a rental offer if caller is not the lender', async () => { await expect(marketplace.connect(notOperator).createRentalOffer(rentalOffer)).to.be.revertedWith( 'OriumSftMarketplace: Sender and Lender mismatch', @@ -896,6 +973,37 @@ describe('OriumSftMarketplace', () => { commitAndGrantRoleParams[0].data, ) }) + it('Should use IERC7589Legacy if tokenAddress matches wearableAddress and Commit Tokens', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + commitAndGrantRoleParams[0].tokenAddress = await wearableToken.getAddress() + + await SftRolesRegistrySingleRoleLegacy.connect(lender).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + await wearableToken + .connect(lender) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + await expect(marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams)) + .to.emit(SftRolesRegistrySingleRoleLegacy, 'TokensCommitted') + .withArgs(lender.address, 1, await wearableToken.getAddress(), tokenId, tokenAmount) + .to.emit(SftRolesRegistrySingleRoleLegacy, 'RoleGranted') + .withArgs( + 1, + commitAndGrantRoleParams[0].role, + commitAndGrantRoleParams[0].grantee, + commitAndGrantRoleParams[0].expirationDate, + commitAndGrantRoleParams[0].revocable, + commitAndGrantRoleParams[0].data, + ) + }) + it('Should only grant role when a commitmentId is passed', async () => { await rolesRegistry.setTokenAddressAllowed(commitAndGrantRoleParams[0].tokenAddress, true) diff --git a/test/fixtures/OriumSftMarketplaceFixture.ts b/test/fixtures/OriumSftMarketplaceFixture.ts index eeb881b..b6194a7 100644 --- a/test/fixtures/OriumSftMarketplaceFixture.ts +++ b/test/fixtures/OriumSftMarketplaceFixture.ts @@ -5,6 +5,7 @@ import { OriumSftMarketplace, SftRolesRegistrySingleRole, MockERC1155, + SftRolesRegistrySingleRoleLegacy, } from '../../typechain-types' /** @@ -25,6 +26,10 @@ export async function deploySftMarketplaceContracts() { const rolesRegistry: SftRolesRegistrySingleRole = await RolesRegistryFactory.deploy(marketplaceaddress) await rolesRegistry.waitForDeployment() + const SftRolesRegistrySingleRoleLegacy = await ethers.getContractFactory('SftRolesRegistrySingleRoleLegacy') + const rolesRegistryLegacy: SftRolesRegistrySingleRoleLegacy = await SftRolesRegistrySingleRoleLegacy.deploy() + await rolesRegistryLegacy.waitForDeployment() + const MarketplaceRoyaltiesFactory = await ethers.getContractFactory('OriumMarketplaceRoyalties') const marketplaceRoyaltiesProxy = await upgrades.deployProxy(MarketplaceRoyaltiesFactory, [ operator.address, @@ -60,9 +65,9 @@ export async function deploySftMarketplaceContracts() { await marketplaceProxy.getAddress(), ) - // const MockERC1155Factory = await ethers.getContractFactory('MockERC1155') - // const mockERC1155 = await MockERC1155Factory.deploy() - // await mockERC1155.waitForDeployment() + const WaerableFactory = await ethers.getContractFactory('MockERC1155') + const waereableToken = await WaerableFactory.deploy() + await waereableToken.waitForDeployment() const secondMockERC1155 = await MockERC1155Factory.deploy() await secondMockERC1155.waitForDeployment() @@ -71,5 +76,14 @@ export async function deploySftMarketplaceContracts() { const mockERC20 = await MockERC20Factory.deploy() await mockERC20.waitForDeployment() - return [marketplace, marketplaceRoyalties, rolesRegistry, mockERC1155, mockERC20, secondMockERC1155] as const + return [ + marketplace, + marketplaceRoyalties, + rolesRegistry, + mockERC1155, + mockERC20, + secondMockERC1155, + waereableToken, + rolesRegistryLegacy, + ] as const } From e9699d227378daa3c64b4d303cc8db1ae8f62953 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Mon, 8 Jul 2024 11:51:29 -0300 Subject: [PATCH 05/13] fix: updating mock roles registry implementation --- contracts/mocks/SftRolesRegistrySingleRole.sol | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/contracts/mocks/SftRolesRegistrySingleRole.sol b/contracts/mocks/SftRolesRegistrySingleRole.sol index 4a5c1ac..9456da4 100644 --- a/contracts/mocks/SftRolesRegistrySingleRole.sol +++ b/contracts/mocks/SftRolesRegistrySingleRole.sol @@ -142,11 +142,9 @@ contract SftRolesRegistrySingleRole is IERC7589, ERC1155Holder, IERC7589LockToke uint256 _tokenId = tokenLocks[_lockId].tokenId; uint256 _tokenAmount = tokenLocks[_lockId].tokenAmount; delete tokenLocks[_lockId]; - emit TokensUnlocked(_lockId); - _transferFrom(address(this), _owner, _tokenAddress, _tokenId, _tokenAmount); - - } + emit TokensUnlocked(_lockId); + } function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _approved) external { roleApprovals[msg.sender][_tokenAddress][_operator] = _approved; @@ -254,9 +252,9 @@ contract SftRolesRegistrySingleRole is IERC7589, ERC1155Holder, IERC7589LockToke require(_tokenAmount > 0, 'ERC7589RolesRegistry: tokenAmount must be greater than zero'); lockId_ = ++lockIdCount; tokenLocks[lockId_] = TokenLock(_owner, _tokenAddress, _tokenId, _tokenAmount); - emit TokensLocked(_owner, lockId_, _tokenAddress, _tokenId, _tokenAmount); _transferFrom(_owner, address(this), _tokenAddress, _tokenId, _tokenAmount); - } + emit TokensLocked(_owner, lockId_, _tokenAddress, _tokenId, _tokenAmount); + } function _grantERC7589Role( uint256 _lockId, From 6c42c0f9a1f26b855b6cfd343e3be1e2cf31d181 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Tue, 9 Jul 2024 11:39:30 -0300 Subject: [PATCH 06/13] fix: updating test case to cover 100% of tests --- contracts/OriumSftMarketplace.sol | 2 +- test/OriumSftMarketplace.test.ts | 44 +++++++++---------------------- 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/contracts/OriumSftMarketplace.sol b/contracts/OriumSftMarketplace.sol index 639f2aa..e454526 100644 --- a/contracts/OriumSftMarketplace.sol +++ b/contracts/OriumSftMarketplace.sol @@ -139,7 +139,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra _offer.tokenId, _offer.tokenAmount ); - }else { + } else { _offer.commitmentId = IERC7589(_rolesRegistryAddress).lockTokens( _offer.lender, _offer.tokenAddress, diff --git a/test/OriumSftMarketplace.test.ts b/test/OriumSftMarketplace.test.ts index 769624c..e9a4cd3 100644 --- a/test/OriumSftMarketplace.test.ts +++ b/test/OriumSftMarketplace.test.ts @@ -208,37 +208,6 @@ describe('OriumSftMarketplace', () => { .to.emit(rolesRegistry, 'TokensLocked') .withArgs(lender.address, 1, await mockERC1155.getAddress(), tokenId, tokenAmount) }) - it('Should create a rental offer with feeAmountPerSecond equal to 0 if offer is private', async function () { - rentalOffer.feeAmountPerSecond = BigInt(0) - rentalOffer.borrower = lender.address - await expect(marketplace.connect(lender).createRentalOffer(rentalOffer)) - .to.emit(marketplace, 'RentalOfferCreated') - .withArgs( - rentalOffer.nonce, - rentalOffer.tokenAddress, - rentalOffer.tokenId, - rentalOffer.tokenAmount, - 1, - rentalOffer.lender, - rentalOffer.borrower, - rentalOffer.feeTokenAddress, - rentalOffer.feeAmountPerSecond, - rentalOffer.deadline, - rentalOffer.minDuration, - rentalOffer.roles, - rentalOffer.rolesData, - ) - .to.emit(mockERC1155, 'TransferSingle') - .withArgs( - await rolesRegistry.getAddress(), - lender.address, - await rolesRegistry.getAddress(), - tokenId, - tokenAmount, - ) - .to.emit(rolesRegistry, 'TokensLocked') - .withArgs(lender.address, 1, await mockERC1155.getAddress(), tokenId, tokenAmount) - }) it('Should use IERC7589Legacy if tokenAddress matches wearableAddress', async () => { await marketplaceRoyalties .connect(operator) @@ -495,6 +464,19 @@ describe('OriumSftMarketplace', () => { .to.emit(marketplace, 'RentalStarted') .withArgs(rentalOffer.lender, rentalOffer.nonce, borrower.address, expirationDate) }) + it('Should create a rental offer with feeAmountPerSecond equal to 0 if offer is private', async function () { + rentalOffer.feeAmountPerSecond = BigInt(0) + rentalOffer.borrower = borrower.address + rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` + await marketplace.connect(lender).createRentalOffer({ ...rentalOffer, commitmentId: BigInt(0) }) + rentalOffer.commitmentId = BigInt(2) + + const blockTimestamp = (await ethers.provider.getBlock('latest'))?.timestamp + const expirationDate = Number(blockTimestamp) + duration + 1 + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)) + .to.emit(marketplace, 'RentalStarted') + .withArgs(rentalOffer.lender, rentalOffer.nonce, borrower.address, expirationDate) + }) it('Should accept a private rental offer', async () => { rentalOffer.borrower = borrower.address rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` From f3895f30f29a91a79703b94c1d002fbe951754a3 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Thu, 11 Jul 2024 00:26:31 -0300 Subject: [PATCH 07/13] fix: updating orium sft marketplace and library and tests --- contracts/OriumSftMarketplace.sol | 156 +++++++----- ...ERC7589LockTokensAndGrantRoleExtension.sol | 31 --- .../libraries/LibOriumSftMarketplace.sol | 115 +++++---- ...ommitTokensAndGrantRoleExtensionLegacy.sol | 28 --- contracts/mocks/IOriumWrapperManager.sol | 47 ---- .../mocks/SftRolesRegistrySingleRole.sol | 23 +- .../SftRolesRegistrySingleRoleLegacy.sol | 27 +- test/OriumSftMarketplace.test.ts | 232 ++++++++++++++---- test/fixtures/OriumSftMarketplaceFixture.ts | 8 +- 9 files changed, 354 insertions(+), 313 deletions(-) delete mode 100644 contracts/interfaces/IERC7589LockTokensAndGrantRoleExtension.sol delete mode 100644 contracts/mocks/ICommitTokensAndGrantRoleExtensionLegacy.sol delete mode 100644 contracts/mocks/IOriumWrapperManager.sol diff --git a/contracts/OriumSftMarketplace.sol b/contracts/OriumSftMarketplace.sol index e454526..dbd9aed 100644 --- a/contracts/OriumSftMarketplace.sol +++ b/contracts/OriumSftMarketplace.sol @@ -22,8 +22,8 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra /// @dev oriumMarketplaceRoyalties stores the collection royalties and fees address public oriumMarketplaceRoyalties; - /// @dev Wearable address - address public constant wearableAddress = 0x58de9AaBCaeEC0f69883C94318810ad79Cc6a44f; + /// @dev Aavegotchi Wearable address (legacy, only valid on Polygon) + address public constant aavegotchiWearableAddress = 0x58de9AaBCaeEC0f69883C94318810ad79Cc6a44f; /// @dev hashedOffer => bool mapping(bytes32 => bool) public isCreated; @@ -132,21 +132,13 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra _validateCreateRentalOffer(_offer, _rolesRegistryAddress); if (_offer.commitmentId == 0) { - if(_offer.tokenAddress == wearableAddress ){ - _offer.commitmentId = IERC7589Legacy(_rolesRegistryAddress).commitTokens( - _offer.lender, + _offer.commitmentId = _commitOrLockTokens( _offer.tokenAddress, - _offer.tokenId, - _offer.tokenAmount - ); - } else { - _offer.commitmentId = IERC7589(_rolesRegistryAddress).lockTokens( + _rolesRegistryAddress, _offer.lender, - _offer.tokenAddress, _offer.tokenId, _offer.tokenAmount - ); - } + ); } nonceDeadline[msg.sender][_offer.nonce] = _offer.deadline; @@ -211,36 +203,6 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra emit RentalStarted(_offer.lender, _offer.nonce, msg.sender, _expirationDate); } - /** - * @notice Cancels a rental offer. - * @param _offer The rental offer struct. It should be the same as the one used to create the offer. - */ - function cancelRentalOffer(RentalOffer calldata _offer) external whenNotPaused { - bytes32 _offerHash = LibOriumSftMarketplace.hashRentalOffer(_offer); - require(isCreated[_offerHash], 'OriumSftMarketplace: Offer not created'); - require(msg.sender == _offer.lender, 'OriumSftMarketplace: Only lender can cancel a rental offer'); - require( - nonceDeadline[_offer.lender][_offer.nonce] > block.timestamp, - 'OriumSftMarketplace: Nonce expired or not used yet' - ); - - IERC7589 _rolesRegistry = IERC7589( - IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) - ); - // if There are no active rentals, release tokens (else, tokens will be released via `batchReleaseTokens`) - // this is ok for single-role tokens, but tokens with multiple roles might revert if another non-revocable role exists - // if token amount is 0, it means that the tokens were already released, trying to release it again would revert the transaction - if ( - rentals[_offerHash].expirationDate < block.timestamp && - _rolesRegistry.tokenAmountOf(_offer.commitmentId) > 0 - ) { - _rolesRegistry.unlockTokens(_offer.commitmentId); - } - - nonceDeadline[msg.sender][_offer.nonce] = uint64(block.timestamp); - emit RentalOfferCancelled(_offer.lender, _offer.nonce); - } - /** * @notice Delist a rental offer. * @param _offer The rental offer struct. It should be the same as the one used to create the offer. @@ -258,7 +220,15 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra IERC7589 _rolesRegistry = IERC7589( IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) ); - _rolesRegistry.unlockTokens(_offer.commitmentId); + IERC7589Legacy _rolesRegistryLegacy = IERC7589Legacy( + IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) + ); + + if(_offer.tokenAddress == aavegotchiWearableAddress) { + _rolesRegistryLegacy.releaseTokens(_offer.commitmentId); + } else { + _rolesRegistry.unlockTokens(_offer.commitmentId); + } } /** @@ -313,26 +283,16 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra address _rolesRegistryAddress = IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf( _params[i].tokenAddress ); - - IERC7589 _rolesRegistry = IERC7589(_rolesRegistryAddress); - + uint256 _validCommitmentId = _params[i].commitmentId; if (_params[i].commitmentId == 0) { - if(_params[i].tokenAddress == wearableAddress) { - _validCommitmentId = IERC7589Legacy(_rolesRegistryAddress).commitTokens( - msg.sender, + _validCommitmentId = _commitOrLockTokens( _params[i].tokenAddress, - _params[i].tokenId, - _params[i].tokenAmount - ); - } else { - _validCommitmentId = _rolesRegistry.lockTokens( + _rolesRegistryAddress, msg.sender, - _params[i].tokenAddress, _params[i].tokenId, _params[i].tokenAmount ); - } } else { // if the user provided a valid commitmentId, it's necessary to verify if they actually own it, // and if all other parameters passed match the commitmentId @@ -346,7 +306,9 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra ); } - _rolesRegistry.grantRole( + _grantRole( + _params[i].tokenAddress, + _rolesRegistryAddress, _validCommitmentId, _params[i].role, _params[i].grantee, @@ -380,7 +342,83 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra ); } + /** ######### Internals ########### **/ + + /** + * @dev Validates the create rental offer. + * @param _tokenAddress The SFT tokenAddresses. + * @param _rolesRegistryAddress roles registry address. + * @param _lender The address of the user lending the SFT. + * @param _tokenId The tokenId of the SFT to rent. + * @param _tokenAmount The amount of SFT to rent. + */ + function _commitOrLockTokens( + address _tokenAddress, + address _rolesRegistryAddress, + address _lender, + uint256 _tokenId, + uint256 _tokenAmount + ) internal returns (uint256) { + if (_tokenAddress == aavegotchiWearableAddress) { + return IERC7589Legacy(_rolesRegistryAddress).commitTokens( + _lender, + _tokenAddress, + _tokenId, + _tokenAmount + ); + } else { + return IERC7589(_rolesRegistryAddress).lockTokens( + _lender, + _tokenAddress, + _tokenId, + _tokenAmount + ); + } + } + + /** + * @dev Validates the create rental offer. + * @param _tokenAddress The address of the contract of the SFT to rent + * @param _rolesRegistryAddress The rental offer struct. + * @param _commitmentId The commitmentId of the SFT to rent. + * @param _role role to be assigned to the borrower. + * @param _grantee recipient . + * @param _expirationDate expiration date of role. + * @param _revocable role is recovable. + * @param _data data struct to send others informations. + */ + function _grantRole( + address _tokenAddress, + address _rolesRegistryAddress, + uint256 _commitmentId, + bytes32 _role, + address _grantee, + uint64 _expirationDate, + bool _revocable, + bytes memory _data + ) internal { + if (_tokenAddress == aavegotchiWearableAddress) { + IERC7589Legacy(_rolesRegistryAddress).grantRole( + _commitmentId, + _role, + _grantee, + _expirationDate, + _revocable, + _data + ); + } else { + IERC7589(_rolesRegistryAddress).grantRole( + _commitmentId, + _role, + _grantee, + _expirationDate, + _revocable, + _data + ); + } + } + /** * @dev Validates the create rental offer. * @param _offer The rental offer struct. @@ -485,6 +523,8 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra emit RentalOfferCancelled(_offer.lender, _offer.nonce); } + + /** ============================ Core Functions ================================== **/ /** ######### Setters ########### **/ diff --git a/contracts/interfaces/IERC7589LockTokensAndGrantRoleExtension.sol b/contracts/interfaces/IERC7589LockTokensAndGrantRoleExtension.sol deleted file mode 100644 index 90112dd..0000000 --- a/contracts/interfaces/IERC7589LockTokensAndGrantRoleExtension.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -pragma solidity 0.8.9; - -/// @title ERC-7589 Semi-Fungible Token Roles, optional lock tokens and grant role extension -/// @dev See https://eips.ethereum.org/EIPS/eip-7589 -/// Note: the ERC-165 identifier for this interface is 0x0a644ace. -interface IERC7589LockTokensAndGrantRoleExtension { - /// @notice Lock tokens and grant role in a single transaction. - /// @param _owner The owner of the tokens. - /// @param _tokenAddress The token address. - /// @param _tokenId The token identifier. - /// @param _tokenAmount The token amount. - /// @param _roleId The role identifier. - /// @param _recipient The recipient the role. - /// @param _expirationDate The expiration date of the role. - /// @param _revocable Whether the role is revocable. - /// @param _data Any additional data about the role. - /// @return lockId_ The identifier of the locked tokens. - function lockTokensAndGrantRole( - address _owner, - address _tokenAddress, - uint256 _tokenId, - uint256 _tokenAmount, - bytes32 _roleId, - address _recipient, - uint64 _expirationDate, - bool _revocable, - bytes calldata _data - ) external returns (uint256 lockId_); -} \ No newline at end of file diff --git a/contracts/libraries/LibOriumSftMarketplace.sol b/contracts/libraries/LibOriumSftMarketplace.sol index 7640621..99413d7 100644 --- a/contracts/libraries/LibOriumSftMarketplace.sol +++ b/contracts/libraries/LibOriumSftMarketplace.sol @@ -2,9 +2,10 @@ pragma solidity 0.8.9; -import { IERC7589 } from "../interfaces/IERC7589.sol"; -import { IOriumMarketplaceRoyalties } from "../interfaces/IOriumMarketplaceRoyalties.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC7589 } from '../interfaces/IERC7589.sol'; +import { IERC7589Legacy } from '../interfaces/IERC7589Legacy.sol'; +import { IOriumMarketplaceRoyalties } from '../interfaces/IOriumMarketplaceRoyalties.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; /// @dev Rental offer info. struct RentalOffer { @@ -42,6 +43,9 @@ library LibOriumSftMarketplace { /// @dev 2.5 ether is 2.5% uint256 public constant DEFAULT_FEE_PERCENTAGE = 2.5 ether; + /// @dev Aavegotchi Wearable address (legacy, only valid on Polygon) + address public constant aavegotchiWearableAddress = 0x58de9AaBCaeEC0f69883C94318810ad79Cc6a44f; + /** * @notice Gets the rental offer hash. * @dev This function is used to hash the rental offer struct with retrocompatibility. @@ -49,7 +53,7 @@ library LibOriumSftMarketplace { * @param _offer The rental offer struct to be hashed. */ function hashRentalOffer(RentalOffer memory _offer) external pure returns (bytes32) { - return + return _offer.minDuration == 0 ? keccak256( abi.encode( @@ -103,7 +107,7 @@ library LibOriumSftMarketplace { ); require( _rolesRegistry.ownerOf(_commitmentId) == _expectedGrantor, - "OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId" + 'OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId' ); require( _rolesRegistry.tokenAddressOf(_commitmentId) == _tokenAddress, @@ -120,19 +124,19 @@ library LibOriumSftMarketplace { * @param _offer The rental offer struct to be validated. */ function validateOffer(RentalOffer memory _offer) external view { - require(_offer.tokenAmount > 0, "OriumSftMarketplace: tokenAmount should be greater than 0"); - require(_offer.nonce != 0, "OriumSftMarketplace: Nonce cannot be 0"); - require(msg.sender == _offer.lender, "OriumSftMarketplace: Sender and Lender mismatch"); - require(_offer.roles.length > 0, "OriumSftMarketplace: roles should not be empty"); + require(_offer.tokenAmount > 0, 'OriumSftMarketplace: tokenAmount should be greater than 0'); + require(_offer.nonce != 0, 'OriumSftMarketplace: Nonce cannot be 0'); + require(msg.sender == _offer.lender, 'OriumSftMarketplace: Sender and Lender mismatch'); + require(_offer.roles.length > 0, 'OriumSftMarketplace: roles should not be empty'); require( _offer.roles.length == _offer.rolesData.length, - "OriumSftMarketplace: roles and rolesData should have the same length" + 'OriumSftMarketplace: roles and rolesData should have the same length' ); require( _offer.borrower != address(0) || _offer.feeAmountPerSecond > 0, - "OriumSftMarketplace: feeAmountPerSecond should be greater than 0" + 'OriumSftMarketplace: feeAmountPerSecond should be greater than 0' ); - require(_offer.minDuration <= _offer.deadline - block.timestamp, "OriumSftMarketplace: minDuration is invalid"); + require(_offer.minDuration <= _offer.deadline - block.timestamp, 'OriumSftMarketplace: minDuration is invalid'); } /** @@ -158,20 +162,20 @@ library LibOriumSftMarketplace { if (_marketplaceFeeAmount > 0) { require( IERC20(_feeTokenAddress).transferFrom(msg.sender, _marketplaceTreasuryAddress, _marketplaceFeeAmount), - "OriumSftMarketplace: Transfer failed" + 'OriumSftMarketplace: Transfer failed' ); } if (_royaltyAmount > 0) { require( IERC20(_feeTokenAddress).transferFrom(msg.sender, _royaltyTreasuryAddress, _royaltyAmount), - "OriumSftMarketplace: Transfer failed" + 'OriumSftMarketplace: Transfer failed' ); } require( IERC20(_feeTokenAddress).transferFrom(msg.sender, _lenderAddress, _lenderAmount), - "OriumSftMarketplace: Transfer failed" + 'OriumSftMarketplace: Transfer failed' ); } @@ -187,19 +191,28 @@ library LibOriumSftMarketplace { address[] calldata _tokenAddresses, uint256[] calldata _commitmentIds ) external { - require(_tokenAddresses.length == _commitmentIds.length, "OriumSftMarketplace: arrays length mismatch"); + require(_tokenAddresses.length == _commitmentIds.length, 'OriumSftMarketplace: arrays length mismatch'); for (uint256 i = 0; i < _tokenAddresses.length; i++) { address _rolesRegistryAddress = IOriumMarketplaceRoyalties(_oriumMarketplaceRoyaltiesAddress) .sftRolesRegistryOf(_tokenAddresses[i]); - require( - IERC7589(_rolesRegistryAddress).ownerOf(_commitmentIds[i]) == msg.sender, - "OriumSftMarketplace: sender is not the commitment's grantor" - ); - require( - IERC7589(_rolesRegistryAddress).tokenAddressOf(_commitmentIds[i]) == _tokenAddresses[i], - "OriumSftMarketplace: tokenAddress provided does not match commitment's tokenAddress" - ); - IERC7589(_rolesRegistryAddress).unlockTokens(_commitmentIds[i]); + + if (_tokenAddresses[i] == aavegotchiWearableAddress) { + require( + IERC7589Legacy(_rolesRegistryAddress).grantorOf(_commitmentIds[i]) == msg.sender, + "OriumSftMarketplace: sender is not the commitment's grantor Legacy" + ); + IERC7589Legacy(_rolesRegistryAddress).releaseTokens(_commitmentIds[i]); + } else { + require( + IERC7589(_rolesRegistryAddress).ownerOf(_commitmentIds[i]) == msg.sender, + "OriumSftMarketplace: sender is not the commitment's grantor" + ); + require( + IERC7589(_rolesRegistryAddress).tokenAddressOf(_commitmentIds[i]) == _tokenAddresses[i], + "OriumSftMarketplace: tokenAddress provided does not match commitment's tokenAddress" + ); + IERC7589(_rolesRegistryAddress).unlockTokens(_commitmentIds[i]); + } } } @@ -222,31 +235,41 @@ library LibOriumSftMarketplace { _commitmentIds.length == _roles.length && _commitmentIds.length == _grantees.length && _commitmentIds.length == _tokenAddresses.length, - "OriumSftMarketplace: arrays length mismatch" + 'OriumSftMarketplace: arrays length mismatch' ); for (uint256 i = 0; i < _commitmentIds.length; i++) { address _rolesRegistryAddress = IOriumMarketplaceRoyalties(_oriumMarketplaceRoyalties).sftRolesRegistryOf( _tokenAddresses[i] ); - require( - IERC7589(_rolesRegistryAddress).isRoleRevocable(_commitmentIds[i], _roles[i]), - "OriumSftMarketplace: role is not revocable" - ); - require( - IERC7589(_rolesRegistryAddress).roleExpirationDate(_commitmentIds[i], _roles[i]) > block.timestamp, - "OriumSftMarketplace: role is expired" - ); - require( - msg.sender == _grantees[i] || IERC7589(_rolesRegistryAddress).ownerOf(_commitmentIds[i]) == msg.sender, - "OriumSftMarketplace: sender is not the commitment's grantor or grantee" - ); - require( - IERC7589(_rolesRegistryAddress).tokenAddressOf(_commitmentIds[i]) == _tokenAddresses[i], - "OriumSftMarketplace: tokenAddress provided does not match commitment's tokenAddress" - ); - IERC7589(_rolesRegistryAddress).revokeRole(_commitmentIds[i], _roles[i], _grantees[i]); + if (_tokenAddresses[i] == aavegotchiWearableAddress) { + require( + msg.sender == _grantees[i] || + IERC7589Legacy(_rolesRegistryAddress).grantorOf(_commitmentIds[i]) == msg.sender, + "OriumSftMarketplace: sender is not the commitment's grantor or grantee Legacy" + ); + require( + IERC7589Legacy(_rolesRegistryAddress).isRoleRevocable(_commitmentIds[i], _roles[i], _grantees[i]), + 'OriumSftMarketplace: role is not revocable Legacy' + ); + IERC7589Legacy(_rolesRegistryAddress).revokeRole(_commitmentIds[i], _roles[i], _grantees[i]); + } else { + require( + msg.sender == _grantees[i] || + IERC7589(_rolesRegistryAddress).ownerOf(_commitmentIds[i]) == msg.sender, + "OriumSftMarketplace: sender is not the commitment's grantor or grantee" + ); + require( + IERC7589(_rolesRegistryAddress).isRoleRevocable(_commitmentIds[i], _roles[i]), + 'OriumSftMarketplace: role is not revocable' + ); + require( + IERC7589(_rolesRegistryAddress).tokenAddressOf(_commitmentIds[i]) == _tokenAddresses[i], + "OriumSftMarketplace: tokenAddress provided does not match commitment's tokenAddress" + ); + IERC7589(_rolesRegistryAddress).revokeRole(_commitmentIds[i], _roles[i], _grantees[i]); + } } } @@ -260,11 +283,11 @@ library LibOriumSftMarketplace { uint64 _expirationDate ) external view { require(_isCreated, 'OriumSftMarketplace: Offer not created'); - require(_previousRentalExpirationDate <= block.timestamp, 'OriumSftMarketplace: This offer has an ongoing rental'); require( - _duration >= _minDuration, - 'OriumSftMarketplace: Duration is less than the offer minimum duration' + _previousRentalExpirationDate <= block.timestamp, + 'OriumSftMarketplace: This offer has an ongoing rental' ); + require(_duration >= _minDuration, 'OriumSftMarketplace: Duration is less than the offer minimum duration'); require( _nonceDeadline > _expirationDate, 'OriumSftMarketplace: expiration date is greater than offer deadline' diff --git a/contracts/mocks/ICommitTokensAndGrantRoleExtensionLegacy.sol b/contracts/mocks/ICommitTokensAndGrantRoleExtensionLegacy.sol deleted file mode 100644 index d62c2e0..0000000 --- a/contracts/mocks/ICommitTokensAndGrantRoleExtensionLegacy.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -pragma solidity 0.8.9; - -interface ICommitTokensAndGrantRoleExtensionLegacy { - /// @notice Commits tokens and grant role in a single transaction. - /// @param _grantor The owner of the SFTs. - /// @param _tokenAddress The token address. - /// @param _tokenId The token identifier. - /// @param _tokenAmount The token amount. - /// @param _role The role identifier. - /// @param _grantee The recipient 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. - /// @return commitmentId_ The identifier of the commitment created. - function commitTokensAndGrantRole( - address _grantor, - address _tokenAddress, - uint256 _tokenId, - uint256 _tokenAmount, - bytes32 _role, - address _grantee, - uint64 _expirationDate, - bool _revocable, - bytes calldata _data - ) external returns (uint256 commitmentId_); -} \ No newline at end of file diff --git a/contracts/mocks/IOriumWrapperManager.sol b/contracts/mocks/IOriumWrapperManager.sol deleted file mode 100644 index 8284d03..0000000 --- a/contracts/mocks/IOriumWrapperManager.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -pragma solidity 0.8.9; - -interface IOriumWrapperManager { - /** External Functions **/ - - /// @notice Maps a token to a wrapped token. - /// @param _tokenAddress The token address. - /// @param _wrappedTokenAddress The wrapped token address. - function mapToken(address _tokenAddress, address _wrappedTokenAddress) external; - - /// @notice Unmaps a token (removes association from storage). - /// @param _tokenAddress The token address. - function unmapToken(address _tokenAddress) external; - - /// @notice Sets the maximum duration for a token. - /// @param _tokenAddress The token address. - /// @param _maxDuration The maximum duration. - function setMaxDuration(address _tokenAddress, uint256 _maxDuration) external; - - /// @notice Sets the marketplace address. - /// @param _marketplaceAddress The marketplace address. - function setMarketplaceAddress(address _marketplaceAddress) external; - - /** View Functions **/ - - /// @notice Gets the marketplace address of a token. - /// @param _tokenAddress The token address. - /// @return The marketplace address. - function getMarketplaceAddressOf(address _tokenAddress) external view returns (address); - - /// @notice Gets the wrapped token of a token. - /// @param _tokenAddress The token address. - /// @return The wrapped token address. - function getWrappedTokenOf(address _tokenAddress) external view returns (address); - - /// @notice Gets the original token of a wrapped token. - /// @param _wrappedTokenAddress The wrapped token address. - /// @return The original token address. - function getOriginalTokenOf(address _wrappedTokenAddress) external view returns (address); - - /// @notice Gets the maximum duration of a token. - /// @param _tokenAddress The token address. - /// @return The maximum duration. - function getMaxDurationOf(address _tokenAddress) external view returns (uint256); -} diff --git a/contracts/mocks/SftRolesRegistrySingleRole.sol b/contracts/mocks/SftRolesRegistrySingleRole.sol index 9456da4..949b0a0 100644 --- a/contracts/mocks/SftRolesRegistrySingleRole.sol +++ b/contracts/mocks/SftRolesRegistrySingleRole.sol @@ -7,11 +7,10 @@ import { IERC1155Receiver } from '@openzeppelin/contracts/token/ERC1155/IERC1155 import { ERC1155Holder, ERC1155Receiver } from '@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol'; import { IERC7589 } from '../interfaces/IERC7589.sol'; import { IERC1155 } from '@openzeppelin/contracts/token/ERC1155/IERC1155.sol'; -import { IERC7589LockTokensAndGrantRoleExtension } from '../interfaces/IERC7589LockTokensAndGrantRoleExtension.sol'; import { Uint64SortedLinkedListLibrary } from './Uint64SortedLinkedListLibrary.sol'; -import { IOriumWrapperManager } from './IOriumWrapperManager.sol'; -contract SftRolesRegistrySingleRole is IERC7589, ERC1155Holder, IERC7589LockTokensAndGrantRoleExtension { + +contract SftRolesRegistrySingleRole is IERC7589, ERC1155Holder { using Uint64SortedLinkedListLibrary for Uint64SortedLinkedListLibrary.List; struct TokenLock { @@ -85,7 +84,6 @@ contract SftRolesRegistrySingleRole is IERC7589, ERC1155Holder, IERC7589LockToke marketplaceAddress = _marketplaceAddress; supportedInterfaces[type(IERC7589).interfaceId] = true; supportedInterfaces[type(IERC1155Receiver).interfaceId] = true; - supportedInterfaces[type(IERC7589LockTokensAndGrantRoleExtension).interfaceId] = true; } /** ERC-7589 External Functions **/ @@ -150,23 +148,6 @@ contract SftRolesRegistrySingleRole is IERC7589, ERC1155Holder, IERC7589LockToke roleApprovals[msg.sender][_tokenAddress][_operator] = _approved; } - /** ERC-7589 Lock Tokens and Grant Role Extension External Functions **/ - - function lockTokensAndGrantRole( - address _owner, - address _tokenAddress, - uint256 _tokenId, - uint256 _tokenAmount, - bytes32 _roleId, - address _recipient, - uint64 _expirationDate, - bool _revocable, - bytes calldata _data - ) external returns (uint256 lockId_) { - lockId_ = _lockTokens(_owner, _tokenAddress, _tokenId, _tokenAmount); - _grantERC7589Role(lockId_, _roleId, _recipient, _expirationDate, _revocable, _data); - } - /** Manager External Functions **/ function setMarketplaceAddress(address _marketplaceAddress) external onlyManager { diff --git a/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol b/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol index f956972..791f311 100644 --- a/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol +++ b/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol @@ -3,16 +3,14 @@ pragma solidity 0.8.9; import { ISftRolesRegistryLegacy } from './ISftRolesRegistryLegacy.sol'; -import { ICommitTokensAndGrantRoleExtensionLegacy } from './ICommitTokensAndGrantRoleExtensionLegacy.sol'; import { IERC165 } from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; import { IERC1155 } from '@openzeppelin/contracts/token/ERC1155/IERC1155.sol'; import { IERC1155Receiver } from '@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol'; - import { ERC1155Holder, ERC1155Receiver } from '@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol'; import { ERC165Checker } from '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol'; // Semi-fungible token (SFT) registry with only one role (UNIQUE_ROLE) -contract SftRolesRegistrySingleRoleLegacy is ISftRolesRegistryLegacy, ERC1155Holder, ICommitTokensAndGrantRoleExtensionLegacy { +contract SftRolesRegistrySingleRoleLegacy is ISftRolesRegistryLegacy, ERC1155Holder { bytes32 public constant UNIQUE_ROLE = keccak256('UNIQUE_ROLE'); uint256 public commitmentCount; @@ -119,26 +117,6 @@ contract SftRolesRegistrySingleRoleLegacy is ISftRolesRegistryLegacy, ERC1155Hol emit RoleApprovalForAll(_tokenAddress, _operator, _isApproved); } - /** Optional External Functions **/ - - function commitTokensAndGrantRole( - address _grantor, - address _tokenAddress, - uint256 _tokenId, - uint256 _tokenAmount, - bytes32 _role, - address _grantee, - uint64 _expirationDate, - bool _revocable, - bytes calldata _data - ) external override onlyOwnerOrApproved(_grantor, _tokenAddress) returns (uint256 commitmentId_) { - require(_tokenAmount > 0, 'SftRolesRegistry: tokenAmount must be greater than zero'); - require(_role == UNIQUE_ROLE, 'SftRolesRegistry: role not supported'); - require(_expirationDate > block.timestamp, 'SftRolesRegistry: expiration date must be in the future'); - commitmentId_ = _createCommitment(_grantor, _tokenAddress, _tokenId, _tokenAmount); - _grantOrUpdateRole(commitmentId_, _role, _grantee, _expirationDate, _revocable, _data); - } - /** View Functions **/ function grantorOf(uint256 _commitmentId) external view returns (address grantor_) { @@ -194,8 +172,7 @@ contract SftRolesRegistrySingleRoleLegacy is ISftRolesRegistryLegacy, ERC1155Hol ) public view virtual override(ERC1155Receiver, IERC165) returns (bool) { return interfaceId == type(ISftRolesRegistryLegacy).interfaceId || - interfaceId == type(IERC1155Receiver).interfaceId || - interfaceId == type(ICommitTokensAndGrantRoleExtensionLegacy).interfaceId; + interfaceId == type(IERC1155Receiver).interfaceId; } /** Helper Functions **/ diff --git a/test/OriumSftMarketplace.test.ts b/test/OriumSftMarketplace.test.ts index e9a4cd3..b1d46a5 100644 --- a/test/OriumSftMarketplace.test.ts +++ b/test/OriumSftMarketplace.test.ts @@ -348,6 +348,12 @@ describe('OriumSftMarketplace', () => { 'OriumSftMarketplace: feeAmountPerSecond should be greater than 0', ) }) + it('Should NOT create a rental offer if minDuration is invalid', async function () { + rentalOffer.minDuration = Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY * 2 + await expect(marketplace.connect(lender).createRentalOffer(rentalOffer)).to.be.revertedWith( + 'OriumSftMarketplace: minDuration is invalid', + ) + }) it("Should NOT create a rental offer if lender doesn't have enough balance", async () => { const balance = await mockERC1155.balanceOf(lender.address, tokenId) await mockERC1155 @@ -651,57 +657,47 @@ describe('OriumSftMarketplace', () => { }) }) - describe('Cancel Rental Offer', async () => { - it('Should cancel a rental offer and releaseTokens from rolesRegistry', async () => { - await expect(marketplace.connect(lender).cancelRentalOffer(rentalOffer)) + describe('Delist Rental Offer and Withdraw', async () => { + it('Should delist a rental offer and releaseTokens from rolesRegistry', async () => { + await expect(marketplace.connect(lender).delistRentalOfferAndWithdraw(rentalOffer)) .to.emit(marketplace, 'RentalOfferCancelled') .withArgs(rentalOffer.lender, rentalOffer.nonce) .to.emit(rolesRegistry, 'TokensUnlocked') .withArgs(rentalOffer.commitmentId) }) - it('Should cancel a rental offer if tokens was released before directly from registry', async () => { - await rolesRegistry.connect(lender).unlockTokens(rentalOffer.commitmentId) - await expect(marketplace.connect(lender).cancelRentalOffer(rentalOffer)) - .to.emit(marketplace, 'RentalOfferCancelled') - .withArgs(rentalOffer.lender, rentalOffer.nonce) - }) - it('Should NOT cancel a rental offer if contract is paused', async () => { - await marketplace.connect(operator).pause() - await expect(marketplace.connect(borrower).cancelRentalOffer(rentalOffer)).to.be.revertedWith( - 'Pausable: paused', - ) - }) - it('Should NOT cancel a rental offer if nonce not used yet by caller', async () => { - await expect(marketplace.connect(notOperator).cancelRentalOffer(rentalOffer)).to.be.revertedWith( - 'OriumSftMarketplace: Only lender can cancel a rental offer', - ) - }) - it("Should NOT cancel a rental offer after deadline's expiration", async () => { - // move forward in time to expire the offer - const blockTimestamp = (await ethers.provider.getBlock('latest'))?.timestamp - const timeToMove = rentalOffer.deadline - Number(blockTimestamp) + 1 - await ethers.provider.send('evm_increaseTime', [timeToMove]) + it('Should delist a LEGACY rental offer and releaseTokens from rolesRegistry', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) - await expect(marketplace.connect(lender).cancelRentalOffer(rentalOffer)).to.be.revertedWith( - 'OriumSftMarketplace: Nonce expired or not used yet', - ) - }) - it("Should NOT cancel a rental offer if it's not created", async () => { - await expect( - marketplace - .connect(lender) - .cancelRentalOffer({ ...rentalOffer, nonce: `0x${randomBytes(32).toString('hex')}` }), - ).to.be.revertedWith('OriumSftMarketplace: Offer not created') - }) - }) + const rentalOfferLegacy = { + nonce: `0x${randomBytes(32).toString('hex')}`, + commitmentId: BigInt(0), + lender: lender.address, + borrower: AddressZero, + tokenAddress: wearableAddress, + tokenId, + tokenAmount, + feeTokenAddress: await mockERC20.getAddress(), + feeAmountPerSecond: toWei('0.0000001'), + deadline: Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY, + minDuration: 1, + roles: [UNIQUE_ROLE], + rolesData: [EMPTY_BYTES], + } - describe('Delist Rental Offer and Withdraw', async () => { - it('Should delist a rental offer and releaseTokens from rolesRegistry', async () => { - await expect(marketplace.connect(lender).delistRentalOfferAndWithdraw(rentalOffer)) + totalFeeAmount = rentalOfferLegacy.feeAmountPerSecond * BigInt(duration) + await marketplace.connect(lender).createRentalOffer(rentalOfferLegacy) + rentalOfferLegacy.commitmentId = BigInt(1) + await mockERC20.mint(borrower.address, totalFeeAmount.toString()) + await mockERC20.connect(borrower).approve(await marketplace.getAddress(), totalFeeAmount.toString()) + + await expect(marketplace.connect(lender).delistRentalOfferAndWithdraw(rentalOfferLegacy)) .to.emit(marketplace, 'RentalOfferCancelled') - .withArgs(rentalOffer.lender, rentalOffer.nonce) - .to.emit(rolesRegistry, 'TokensUnlocked') - .withArgs(rentalOffer.commitmentId) + .withArgs(rentalOfferLegacy.lender, rentalOfferLegacy.nonce) + .to.emit(SftRolesRegistrySingleRoleLegacy, 'TokensReleased') + .withArgs(rentalOfferLegacy.commitmentId) }) it('Should NOT delist a rental offer if tokens was released before directly from registry', async () => { await rolesRegistry.connect(lender).unlockTokens(rentalOffer.commitmentId) @@ -784,6 +780,7 @@ describe('OriumSftMarketplace', () => { describe('Batch Release Tokens', async () => { it('Should release tokens from rolesRegistry', async () => { + 2 await time.increase(ONE_DAY) await expect( marketplace.connect(lender).batchReleaseTokens([rentalOffer.tokenAddress], [rentalOffer.commitmentId]), @@ -791,6 +788,63 @@ describe('OriumSftMarketplace', () => { .to.emit(rolesRegistry, 'TokensUnlocked') .withArgs(rentalOffer.commitmentId) }) + + it('Should release LEGACY tokens from rolesRegistry', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + const rentalOfferLegacy = { + nonce: `0x${randomBytes(32).toString('hex')}`, + commitmentId: BigInt(0), + lender: lender.address, + borrower: AddressZero, + tokenAddress: wearableAddress, + tokenId, + tokenAmount, + feeTokenAddress: await mockERC20.getAddress(), + feeAmountPerSecond: toWei('0.0000001'), + deadline: Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY, + minDuration: 1, + roles: [UNIQUE_ROLE], + rolesData: [EMPTY_BYTES], + } + expect(marketplace.connect(lender).createRentalOffer(rentalOfferLegacy)) + rentalOfferLegacy.commitmentId = BigInt(1) + await expect( + marketplace + .connect(lender) + .batchReleaseTokens([rentalOfferLegacy.tokenAddress], [rentalOfferLegacy.commitmentId]), + ) + .to.emit(SftRolesRegistrySingleRoleLegacy, 'TokensReleased') + .withArgs(rentalOfferLegacy.commitmentId) + }) + it('Should NOT release Legacy tokens if sender is not the grantor', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + const rentalOfferLegacy = { + nonce: `0x${randomBytes(32).toString('hex')}`, + commitmentId: BigInt(0), + lender: lender.address, + borrower: AddressZero, + tokenAddress: wearableAddress, + tokenId, + tokenAmount, + feeTokenAddress: await mockERC20.getAddress(), + feeAmountPerSecond: toWei('0.0000001'), + deadline: Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY, + minDuration: 1, + roles: [UNIQUE_ROLE], + rolesData: [EMPTY_BYTES], + } + expect(marketplace.connect(lender).createRentalOffer(rentalOfferLegacy)) + rentalOfferLegacy.commitmentId = BigInt(1) + await expect( + marketplace + .connect(borrower) + .batchReleaseTokens([rentalOfferLegacy.tokenAddress], [rentalOfferLegacy.commitmentId]), + ).to.be.revertedWith("OriumSftMarketplace: sender is not the commitment's grantor Legacy") + }) it('Should NOT release tokens if contract is paused', async () => { await marketplace.connect(operator).pause() await expect( @@ -883,15 +937,6 @@ describe('OriumSftMarketplace', () => { }) }) - describe('Cancel Rental Offer', async function () { - it('Should cancel a rental offer if it has an active rental but NOT releaseTokens from rolesRegistry', async () => { - await expect(marketplace.connect(lender).cancelRentalOffer(rentalOffer)) - .to.emit(marketplace, 'RentalOfferCancelled') - .withArgs(rentalOffer.lender, rentalOffer.nonce) - .to.not.emit(rolesRegistry, 'TokensUnlocked') - }) - }) - describe('Batch Release Tokens', async () => { it('Should release tokens after rental is ended and rental offer expired', async () => { await marketplace.connect(borrower).endRental(rentalOffer) @@ -955,7 +1000,7 @@ describe('OriumSftMarketplace', () => { commitAndGrantRoleParams[0].data, ) }) - it('Should use IERC7589Legacy if tokenAddress matches wearableAddress and Commit Tokens', async () => { + it('Should use IERC7589Legacy if tokenAddress matches wearableAddress and Bach Commit Tokens ', async () => { await marketplaceRoyalties .connect(operator) .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) @@ -1118,6 +1163,29 @@ describe('OriumSftMarketplace', () => { .to.emit(rolesRegistry, 'RoleRevoked') .withArgs(1, UNIQUE_ROLE, borrower.address) }) + it('Should use IERC7589Legacy if tokenAddress matches wearableAddress and Batch Revole Role ', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + commitAndGrantRoleParams[0].tokenAddress = await wearableToken.getAddress() + + await SftRolesRegistrySingleRoleLegacy.connect(lender).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + expect(marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams)) + + await expect( + marketplace + .connect(lender) + .batchRevokeRole([1], [UNIQUE_ROLE], [borrower.address], [await wearableToken.getAddress()]), + ) + .to.emit(SftRolesRegistrySingleRoleLegacy, 'RoleRevoked') + .withArgs(1, UNIQUE_ROLE, borrower.address) + }) it("Should NOT batch revoke role if sender is not commitment's grantor neither grantee", async () => { await expect( marketplace @@ -1125,6 +1193,27 @@ describe('OriumSftMarketplace', () => { .batchRevokeRole([1], [UNIQUE_ROLE], [borrower.address], [await mockERC1155.getAddress()]), ).to.be.revertedWith("OriumSftMarketplace: sender is not the commitment's grantor or grantee") }) + it("Should NOT batch LEGACY revoke role if sender is not commitment's grantor neither grantee", async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + commitAndGrantRoleParams[0].tokenAddress = await wearableToken.getAddress() + + await SftRolesRegistrySingleRoleLegacy.connect(lender).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + expect(marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams)) + + await expect( + marketplace + .connect(notOperator) + .batchRevokeRole([1], [UNIQUE_ROLE], [borrower.address], [await wearableToken.getAddress()]), + ).to.be.revertedWith("OriumSftMarketplace: sender is not the commitment's grantor or grantee Legacy") + }) it('Should NOT batch revoke role if tokenAddress does not match commitment', async () => { await expect( marketplace.connect(lender).batchRevokeRole([1], [UNIQUE_ROLE], [borrower.address], [AddressZero]), @@ -1152,6 +1241,43 @@ describe('OriumSftMarketplace', () => { .batchRevokeRole([1], [UNIQUE_ROLE], [borrower.address], [await mockERC1155.getAddress()]), ).to.be.revertedWith('OriumSftMarketplace: role is not revocable') }) + it('Should NOT batch LEGACY revoke role if role is not revocable', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + commitAndGrantRoleParams[0].tokenAddress = await wearableToken.getAddress() + + await SftRolesRegistrySingleRoleLegacy.connect(lender).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + await wearableToken + .connect(lender) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + expect(marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams)) + + await SftRolesRegistrySingleRoleLegacy.connect(lender).grantRole( + 1, + UNIQUE_ROLE, + borrower.address, + Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY, + false, + EMPTY_BYTES, + ) + await SftRolesRegistrySingleRoleLegacy.connect(borrower).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + await expect( + marketplace + .connect(lender) + .batchRevokeRole([1], [UNIQUE_ROLE], [borrower.address], [await wearableToken.getAddress()]), + ).to.be.revertedWith('OriumSftMarketplace: role is not revocable Legacy') + }) }) }) }) diff --git a/test/fixtures/OriumSftMarketplaceFixture.ts b/test/fixtures/OriumSftMarketplaceFixture.ts index b6194a7..073b6e1 100644 --- a/test/fixtures/OriumSftMarketplaceFixture.ts +++ b/test/fixtures/OriumSftMarketplaceFixture.ts @@ -65,9 +65,9 @@ export async function deploySftMarketplaceContracts() { await marketplaceProxy.getAddress(), ) - const WaerableFactory = await ethers.getContractFactory('MockERC1155') - const waereableToken = await WaerableFactory.deploy() - await waereableToken.waitForDeployment() + const WearableFactory = await ethers.getContractFactory('MockERC1155') + const wearableToken = await WearableFactory.deploy() + await wearableToken.waitForDeployment() const secondMockERC1155 = await MockERC1155Factory.deploy() await secondMockERC1155.waitForDeployment() @@ -83,7 +83,7 @@ export async function deploySftMarketplaceContracts() { mockERC1155, mockERC20, secondMockERC1155, - waereableToken, + wearableToken, rolesRegistryLegacy, ] as const } From eb5cd2efb3a830d5979984c3ab97a7837451a3f8 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Thu, 11 Jul 2024 12:02:14 -0300 Subject: [PATCH 08/13] fix: some contracts structures --- contracts/OriumSftMarketplace.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/OriumSftMarketplace.sol b/contracts/OriumSftMarketplace.sol index dbd9aed..9834c6c 100644 --- a/contracts/OriumSftMarketplace.sol +++ b/contracts/OriumSftMarketplace.sol @@ -346,7 +346,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra /** ######### Internals ########### **/ /** - * @dev Validates the create rental offer. + * @dev Validates if is wearable address to use commit or lock functions. * @param _tokenAddress The SFT tokenAddresses. * @param _rolesRegistryAddress roles registry address. * @param _lender The address of the user lending the SFT. @@ -378,12 +378,12 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra } /** - * @dev Validates the create rental offer. - * @param _tokenAddress The address of the contract of the SFT to rent + * @dev Validates if is wearable address to use current or legacy grantRole. + * @param _tokenAddress The address of the contract of the SFT to rent. * @param _rolesRegistryAddress The rental offer struct. * @param _commitmentId The commitmentId of the SFT to rent. * @param _role role to be assigned to the borrower. - * @param _grantee recipient . + * @param _grantee recipient. * @param _expirationDate expiration date of role. * @param _revocable role is recovable. * @param _data data struct to send others informations. From b2ef8a90bb3591e307f5c3d993a233bab144f80a Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Fri, 12 Jul 2024 13:28:31 -0300 Subject: [PATCH 09/13] fix: struct logic lib to check grantOf and ownerOf --- contracts/OriumSftMarketplace.sol | 70 +------ .../libraries/LibOriumSftMarketplace.sol | 64 ++++-- contracts/mocks/ISftRolesRegistryLegacy.sol | 182 ------------------ .../SftRolesRegistrySingleRoleLegacy.sol | 6 +- 4 files changed, 55 insertions(+), 267 deletions(-) delete mode 100644 contracts/mocks/ISftRolesRegistryLegacy.sol diff --git a/contracts/OriumSftMarketplace.sol b/contracts/OriumSftMarketplace.sol index 9834c6c..434e4d2 100644 --- a/contracts/OriumSftMarketplace.sol +++ b/contracts/OriumSftMarketplace.sol @@ -138,7 +138,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra _offer.lender, _offer.tokenId, _offer.tokenAmount - ); + ); } nonceDeadline[msg.sender][_offer.nonce] = _offer.deadline; @@ -224,7 +224,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) ); - if(_offer.tokenAddress == aavegotchiWearableAddress) { + if (_offer.tokenAddress == aavegotchiWearableAddress) { _rolesRegistryLegacy.releaseTokens(_offer.commitmentId); } else { _rolesRegistry.unlockTokens(_offer.commitmentId); @@ -283,7 +283,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra address _rolesRegistryAddress = IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf( _params[i].tokenAddress ); - + uint256 _validCommitmentId = _params[i].commitmentId; if (_params[i].commitmentId == 0) { _validCommitmentId = _commitOrLockTokens( @@ -306,9 +306,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra ); } - _grantRole( - _params[i].tokenAddress, - _rolesRegistryAddress, + IERC7589(_rolesRegistryAddress).grantRole( _validCommitmentId, _params[i].role, _params[i].grantee, @@ -342,7 +340,6 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra ); } - /** ######### Internals ########### **/ /** @@ -361,61 +358,9 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra uint256 _tokenAmount ) internal returns (uint256) { if (_tokenAddress == aavegotchiWearableAddress) { - return IERC7589Legacy(_rolesRegistryAddress).commitTokens( - _lender, - _tokenAddress, - _tokenId, - _tokenAmount - ); - } else { - return IERC7589(_rolesRegistryAddress).lockTokens( - _lender, - _tokenAddress, - _tokenId, - _tokenAmount - ); - } - } - - /** - * @dev Validates if is wearable address to use current or legacy grantRole. - * @param _tokenAddress The address of the contract of the SFT to rent. - * @param _rolesRegistryAddress The rental offer struct. - * @param _commitmentId The commitmentId of the SFT to rent. - * @param _role role to be assigned to the borrower. - * @param _grantee recipient. - * @param _expirationDate expiration date of role. - * @param _revocable role is recovable. - * @param _data data struct to send others informations. - */ - function _grantRole( - address _tokenAddress, - address _rolesRegistryAddress, - uint256 _commitmentId, - bytes32 _role, - address _grantee, - uint64 _expirationDate, - bool _revocable, - bytes memory _data - ) internal { - if (_tokenAddress == aavegotchiWearableAddress) { - IERC7589Legacy(_rolesRegistryAddress).grantRole( - _commitmentId, - _role, - _grantee, - _expirationDate, - _revocable, - _data - ); + return IERC7589Legacy(_rolesRegistryAddress).commitTokens(_lender, _tokenAddress, _tokenId, _tokenAmount); } else { - IERC7589(_rolesRegistryAddress).grantRole( - _commitmentId, - _role, - _grantee, - _expirationDate, - _revocable, - _data - ); + return IERC7589(_rolesRegistryAddress).lockTokens(_lender, _tokenAddress, _tokenId, _tokenAmount); } } @@ -523,8 +468,6 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra emit RentalOfferCancelled(_offer.lender, _offer.nonce); } - - /** ============================ Core Functions ================================== **/ /** ######### Setters ########### **/ @@ -553,6 +496,5 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra function setOriumMarketplaceRoyalties(address _oriumMarketplaceRoyalties) external onlyOwner { oriumMarketplaceRoyalties = _oriumMarketplaceRoyalties; } - /** ######### Getters ########### **/ } diff --git a/contracts/libraries/LibOriumSftMarketplace.sol b/contracts/libraries/LibOriumSftMarketplace.sol index 99413d7..4a171fe 100644 --- a/contracts/libraries/LibOriumSftMarketplace.sol +++ b/contracts/libraries/LibOriumSftMarketplace.sol @@ -101,22 +101,39 @@ library LibOriumSftMarketplace { address _rolesRegistryAddress ) external view { IERC7589 _rolesRegistry = IERC7589(_rolesRegistryAddress); - require( - _rolesRegistry.tokenAmountOf(_commitmentId) == _tokenAmount, - "OriumSftMarketplace: tokenAmount provided does not match commitment's tokenAmount" - ); - require( - _rolesRegistry.ownerOf(_commitmentId) == _expectedGrantor, - 'OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId' - ); - require( - _rolesRegistry.tokenAddressOf(_commitmentId) == _tokenAddress, - "OriumSftMarketplace: tokenAddress provided does not match commitment's tokenAddress" - ); - require( - _rolesRegistry.tokenIdOf(_commitmentId) == _tokenId, - "OriumSftMarketplace: tokenId provided does not match commitment's tokenId" - ); + IERC7589Legacy _rolesRegistryLegacy = IERC7589Legacy(_rolesRegistryAddress); + + if (_tokenAddress == aavegotchiWearableAddress) { + require( + _rolesRegistryLegacy.grantorOf(_commitmentId) == _expectedGrantor, + 'OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId' + ); + require( + _rolesRegistryLegacy.tokenAmountOf(_commitmentId) == _tokenAmount, + "OriumSftMarketplace: tokenAmount provided does not match commitment's tokenAmount" + ); + require( + _rolesRegistryLegacy.tokenIdOf(_commitmentId) == _tokenId, + "OriumSftMarketplace: tokenId provided does not match commitment's tokenId" + ); + } else { + require( + _rolesRegistry.tokenAmountOf(_commitmentId) == _tokenAmount, + "OriumSftMarketplace: tokenAmount provided does not match commitment's tokenAmount" + ); + require( + _rolesRegistry.ownerOf(_commitmentId) == _expectedGrantor, + 'OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId' + ); + require( + _rolesRegistry.tokenAddressOf(_commitmentId) == _tokenAddress, + "OriumSftMarketplace: tokenAddress provided does not match commitment's tokenAddress" + ); + require( + _rolesRegistry.tokenIdOf(_commitmentId) == _tokenId, + "OriumSftMarketplace: tokenId provided does not match commitment's tokenId" + ); + } } /** @@ -249,17 +266,28 @@ library LibOriumSftMarketplace { IERC7589Legacy(_rolesRegistryAddress).grantorOf(_commitmentIds[i]) == msg.sender, "OriumSftMarketplace: sender is not the commitment's grantor or grantee Legacy" ); + require( + IERC7589Legacy(_rolesRegistryAddress).roleExpirationDate( + _commitmentIds[i], + _roles[i], + _grantees[i] + ) > block.timestamp, + 'OriumSftMarketplace: role is expired' + ); require( IERC7589Legacy(_rolesRegistryAddress).isRoleRevocable(_commitmentIds[i], _roles[i], _grantees[i]), 'OriumSftMarketplace: role is not revocable Legacy' ); - IERC7589Legacy(_rolesRegistryAddress).revokeRole(_commitmentIds[i], _roles[i], _grantees[i]); } else { require( msg.sender == _grantees[i] || IERC7589(_rolesRegistryAddress).ownerOf(_commitmentIds[i]) == msg.sender, "OriumSftMarketplace: sender is not the commitment's grantor or grantee" ); + require( + IERC7589(_rolesRegistryAddress).roleExpirationDate(_commitmentIds[i], _roles[i]) > block.timestamp, + 'OriumSftMarketplace: role is expired' + ); require( IERC7589(_rolesRegistryAddress).isRoleRevocable(_commitmentIds[i], _roles[i]), 'OriumSftMarketplace: role is not revocable' @@ -268,8 +296,8 @@ library LibOriumSftMarketplace { IERC7589(_rolesRegistryAddress).tokenAddressOf(_commitmentIds[i]) == _tokenAddresses[i], "OriumSftMarketplace: tokenAddress provided does not match commitment's tokenAddress" ); - IERC7589(_rolesRegistryAddress).revokeRole(_commitmentIds[i], _roles[i], _grantees[i]); } + IERC7589(_rolesRegistryAddress).revokeRole(_commitmentIds[i], _roles[i], _grantees[i]); } } diff --git a/contracts/mocks/ISftRolesRegistryLegacy.sol b/contracts/mocks/ISftRolesRegistryLegacy.sol deleted file mode 100644 index a52f060..0000000 --- a/contracts/mocks/ISftRolesRegistryLegacy.sol +++ /dev/null @@ -1,182 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -pragma solidity 0.8.9; - -import { IERC165 } from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; - -interface ISftRolesRegistryLegacy is IERC165 { - struct RoleAssignment { - address grantee; - uint64 expirationDate; - bool revocable; - bytes data; - } - - struct Commitment { - address grantor; - address tokenAddress; - uint256 tokenId; - uint256 tokenAmount; - } - - /** Events **/ - - /// @notice Emitted when tokens are committed (deposited or frozen). - /// @param _grantor The owner of the SFTs. - /// @param _commitmentId The identifier of the commitment created. - /// @param _tokenAddress The token address. - /// @param _tokenId The token identifier. - /// @param _tokenAmount The token amount. - event TokensCommitted( - address indexed _grantor, - uint256 indexed _commitmentId, - address indexed _tokenAddress, - uint256 _tokenId, - uint256 _tokenAmount - ); - - /// @notice Emitted when a role is granted. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient 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( - uint256 indexed _commitmentId, - bytes32 indexed _role, - address indexed _grantee, - uint64 _expirationDate, - bool _revocable, - bytes _data - ); - - /// @notice Emitted when a role is revoked. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient of the role revocation. - event RoleRevoked(uint256 indexed _commitmentId, bytes32 indexed _role, address indexed _grantee); - - /// @notice Emitted when a user releases tokens from a commitment. - /// @param _commitmentId The commitment identifier. - event TokensReleased(uint256 indexed _commitmentId); - - /// @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); - - /** External Functions **/ - - /// @notice Commits tokens (deposits on a contract or freezes balance). - /// @param _grantor The owner of the SFTs. - /// @param _tokenAddress The token address. - /// @param _tokenId The token identifier. - /// @param _tokenAmount The token amount. - /// @return commitmentId_ The unique identifier of the commitment created. - function commitTokens( - address _grantor, - address _tokenAddress, - uint256 _tokenId, - uint256 _tokenAmount - ) external returns (uint256 commitmentId_); - - /// @notice Grants a role to `_grantee`. - /// @param _commitmentId The identifier of the commitment. - /// @param _role The role identifier. - /// @param _grantee The recipient 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. - function grantRole( - uint256 _commitmentId, - bytes32 _role, - address _grantee, - uint64 _expirationDate, - bool _revocable, - bytes calldata _data - ) external; - - /// @notice Revokes a role. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient of the role revocation. - function revokeRole(uint256 _commitmentId, bytes32 _role, address _grantee) external; - - /// @notice Releases tokens back to grantor. - /// @param _commitmentId The commitment identifier. - function releaseTokens(uint256 _commitmentId) external; - - /// @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. - function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _approved) external; - - /** View Functions **/ - - /// @notice Returns the owner of the commitment (grantor). - /// @param _commitmentId The commitment identifier. - /// @return grantor_ The commitment owner. - function grantorOf(uint256 _commitmentId) external view returns (address grantor_); - - /// @notice Returns the address of the token committed. - /// @param _commitmentId The commitment identifier. - /// @return tokenAddress_ The token address. - function tokenAddressOf(uint256 _commitmentId) external view returns (address tokenAddress_); - - /// @notice Returns the identifier of the token committed. - /// @param _commitmentId The commitment identifier. - /// @return tokenId_ The token identifier. - function tokenIdOf(uint256 _commitmentId) external view returns (uint256 tokenId_); - - /// @notice Returns the amount of tokens committed. - /// @param _commitmentId The commitment identifier. - /// @return tokenAmount_ The token amount. - function tokenAmountOf(uint256 _commitmentId) external view returns (uint256 tokenAmount_); - - /// @notice Returns the custom data of a role assignment. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient the role. - /// @return data_ The custom data. - function roleData( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) external view returns (bytes memory data_); - - /// @notice Returns the expiration date of a role assignment. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient the role. - /// @return expirationDate_ The expiration date. - function roleExpirationDate( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) external view returns (uint64 expirationDate_); - - /// @notice Returns the expiration date of a role assignment. - /// @param _commitmentId The commitment identifier. - /// @param _role The role identifier. - /// @param _grantee The recipient the role. - /// @return revocable_ Whether the role is revocable or not. - function isRoleRevocable( - uint256 _commitmentId, - bytes32 _role, - address _grantee - ) external view returns (bool revocable_); - - /// @notice Checks if the grantor approved the operator for all SFTs. - /// @param _tokenAddress The token address. - /// @param _grantor The user that approved the operator. - /// @param _operator The user that can grant and revoke roles. - /// @return isApproved_ Whether the operator is approved or not. - function isRoleApprovedForAll( - address _tokenAddress, - address _grantor, - address _operator - ) external view returns (bool isApproved_); -} \ No newline at end of file diff --git a/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol b/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol index 791f311..9880a8b 100644 --- a/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol +++ b/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.9; -import { ISftRolesRegistryLegacy } from './ISftRolesRegistryLegacy.sol'; +import { IERC7589Legacy } from '../interfaces/IERC7589Legacy.sol'; import { IERC165 } from '@openzeppelin/contracts/utils/introspection/IERC165.sol'; import { IERC1155 } from '@openzeppelin/contracts/token/ERC1155/IERC1155.sol'; import { IERC1155Receiver } from '@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol'; @@ -10,7 +10,7 @@ import { ERC1155Holder, ERC1155Receiver } from '@openzeppelin/contracts/token/ER import { ERC165Checker } from '@openzeppelin/contracts/utils/introspection/ERC165Checker.sol'; // Semi-fungible token (SFT) registry with only one role (UNIQUE_ROLE) -contract SftRolesRegistrySingleRoleLegacy is ISftRolesRegistryLegacy, ERC1155Holder { +contract SftRolesRegistrySingleRoleLegacy is IERC7589Legacy, ERC1155Holder { bytes32 public constant UNIQUE_ROLE = keccak256('UNIQUE_ROLE'); uint256 public commitmentCount; @@ -171,7 +171,7 @@ contract SftRolesRegistrySingleRoleLegacy is ISftRolesRegistryLegacy, ERC1155Hol bytes4 interfaceId ) public view virtual override(ERC1155Receiver, IERC165) returns (bool) { return - interfaceId == type(ISftRolesRegistryLegacy).interfaceId || + interfaceId == type(IERC7589Legacy).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId; } From e2a6a650200ba4c7712416f05fbbbd67026f0758 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Fri, 12 Jul 2024 14:03:42 -0300 Subject: [PATCH 10/13] feat: adding legacy cases validateCommitmentId --- test/OriumSftMarketplace.test.ts | 115 +++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/test/OriumSftMarketplace.test.ts b/test/OriumSftMarketplace.test.ts index b1d46a5..c2b035e 100644 --- a/test/OriumSftMarketplace.test.ts +++ b/test/OriumSftMarketplace.test.ts @@ -405,6 +405,45 @@ describe('OriumSftMarketplace', () => { 'OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId', ) }) + it("Should NOT create a LEGACY rental offer if commitmentId grantor and offer lender's address are different", async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry( + await wearableToken.getAddress(), + await SftRolesRegistrySingleRoleLegacy.getAddress(), + ) + + await wearableToken.mint(creator.address, tokenId, tokenAmount, '0x') + await wearableToken + .connect(creator) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + const rentalOfferLegacy = { + nonce: `0x${randomBytes(32).toString('hex')}`, + commitmentId: BigInt(0), + lender: lender.address, + borrower: AddressZero, + tokenAddress: wearableAddress, + tokenId, + tokenAmount, + feeTokenAddress: await mockERC20.getAddress(), + feeAmountPerSecond: toWei('0.0000001'), + deadline: Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY, + minDuration: 1, + roles: [UNIQUE_ROLE], + rolesData: [EMPTY_BYTES], + } + await SftRolesRegistrySingleRoleLegacy.connect(creator).commitTokens( + creator.address, + rentalOfferLegacy.tokenAddress, + rentalOfferLegacy.tokenId, + rentalOfferLegacy.tokenAmount, + ) + rentalOfferLegacy.commitmentId = BigInt(2) + await expect(marketplace.connect(lender).createRentalOffer(rentalOfferLegacy)).to.be.revertedWith( + 'OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId', + ) + }) it('Should NOT create a rental offer if commitmentId token address and offer token address are different', async () => { const AnotherMockERC1155 = await ethers.getContractFactory('MockERC1155') const anotherMockERC1155 = await AnotherMockERC1155.deploy() @@ -442,6 +481,53 @@ describe('OriumSftMarketplace', () => { "OriumSftMarketplace: tokenId provided does not match commitment's tokenId", ) }) + it('Should NOT create a LEGACY rental offer if commitmentId token id and offer token id are different', async () => { + const rentalOfferLegacy = { + nonce: `0x${randomBytes(32).toString('hex')}`, + commitmentId: BigInt(0), + lender: lender.address, + borrower: AddressZero, + tokenAddress: wearableAddress, + tokenId, + tokenAmount, + feeTokenAddress: await mockERC20.getAddress(), + feeAmountPerSecond: toWei('0.0000001'), + deadline: Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY, + minDuration: 1, + roles: [UNIQUE_ROLE], + rolesData: [EMPTY_BYTES], + } + + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry( + await wearableToken.getAddress(), + await SftRolesRegistrySingleRoleLegacy.getAddress(), + ) + + await marketplace.connect(lender).createRentalOffer(rentalOfferLegacy) + await time.increase(ONE_DAY) + rentalOfferLegacy.commitmentId = BigInt(1) + rentalOfferLegacy.nonce = `0x${randomBytes(32).toString('hex')}` + rentalOfferLegacy.deadline = (await time.latest()) + ONE_DAY + + const newTokenId = 2 + await wearableToken.mint(lender.address, newTokenId, rentalOfferLegacy.tokenAmount, '0x') + await wearableToken + .connect(lender) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + await SftRolesRegistrySingleRoleLegacy.connect(lender).commitTokens( + lender.address, + rentalOfferLegacy.tokenAddress, + newTokenId, + rentalOfferLegacy.tokenAmount, + ) + rentalOfferLegacy.commitmentId = BigInt(2) + await expect(marketplace.connect(lender).createRentalOffer(rentalOfferLegacy)).to.be.revertedWith( + "OriumSftMarketplace: tokenId provided does not match commitment's tokenId", + ) + }) it('Should NOT create a rental offer if commitmentId token amount and offer token amount are different', async () => { rentalOffer.commitmentId = BigInt(2) rentalOffer.tokenAmount = BigInt(3) @@ -1098,6 +1184,35 @@ describe('OriumSftMarketplace', () => { marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams), ).to.revertedWith("OriumSftMarketplace: tokenAmount provided does not match commitment's tokenAmount") }) + + it('Should NOT commit LEGACY tokens and grant role if tokenAmount does not match the commitment', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + commitAndGrantRoleParams[0].tokenAddress = await wearableToken.getAddress() + + await SftRolesRegistrySingleRoleLegacy.connect(lender).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + await wearableToken + .connect(lender) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + commitAndGrantRoleParams[0].commitmentId = BigInt(1) + commitAndGrantRoleParams[0].tokenAmount = BigInt(0) + await SftRolesRegistrySingleRoleLegacy.connect(lender).commitTokens( + lender.address, + await wearableToken.getAddress(), + tokenId, + tokenAmount, + ) + await expect( + marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams), + ).to.revertedWith("OriumSftMarketplace: tokenAmount provided does not match commitment's tokenAmount") + }) }) }) From f136cc8437d2a2f02c989bf1e5fad9e0e1a0b50a Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Fri, 12 Jul 2024 17:20:58 -0300 Subject: [PATCH 11/13] fix: logic structure of legacy and new ERC7589 --- contracts/OriumSftMarketplace.sol | 12 ++++++------ contracts/libraries/LibOriumSftMarketplace.sol | 5 ++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/contracts/OriumSftMarketplace.sol b/contracts/OriumSftMarketplace.sol index 434e4d2..41fb470 100644 --- a/contracts/OriumSftMarketplace.sol +++ b/contracts/OriumSftMarketplace.sol @@ -217,16 +217,16 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra */ function delistRentalOfferAndWithdraw(RentalOffer calldata _offer) external whenNotPaused { _delistRentalOffer(_offer); - IERC7589 _rolesRegistry = IERC7589( - IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) - ); - IERC7589Legacy _rolesRegistryLegacy = IERC7589Legacy( - IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) - ); if (_offer.tokenAddress == aavegotchiWearableAddress) { + IERC7589Legacy _rolesRegistryLegacy = IERC7589Legacy( + IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) + ); _rolesRegistryLegacy.releaseTokens(_offer.commitmentId); } else { + IERC7589 _rolesRegistry = IERC7589( + IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) + ); _rolesRegistry.unlockTokens(_offer.commitmentId); } } diff --git a/contracts/libraries/LibOriumSftMarketplace.sol b/contracts/libraries/LibOriumSftMarketplace.sol index 4a171fe..7ec04e5 100644 --- a/contracts/libraries/LibOriumSftMarketplace.sol +++ b/contracts/libraries/LibOriumSftMarketplace.sol @@ -100,10 +100,8 @@ library LibOriumSftMarketplace { address _expectedGrantor, address _rolesRegistryAddress ) external view { - IERC7589 _rolesRegistry = IERC7589(_rolesRegistryAddress); - IERC7589Legacy _rolesRegistryLegacy = IERC7589Legacy(_rolesRegistryAddress); - if (_tokenAddress == aavegotchiWearableAddress) { + IERC7589Legacy _rolesRegistryLegacy = IERC7589Legacy(_rolesRegistryAddress); require( _rolesRegistryLegacy.grantorOf(_commitmentId) == _expectedGrantor, 'OriumSftMarketplace: expected grantor does not match the grantor of the commitmentId' @@ -117,6 +115,7 @@ library LibOriumSftMarketplace { "OriumSftMarketplace: tokenId provided does not match commitment's tokenId" ); } else { + IERC7589 _rolesRegistry = IERC7589(_rolesRegistryAddress); require( _rolesRegistry.tokenAmountOf(_commitmentId) == _tokenAmount, "OriumSftMarketplace: tokenAmount provided does not match commitment's tokenAmount" From a41fc47ad23d77c92e0d5f1c201ff4968fc7e8d1 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Fri, 12 Jul 2024 17:55:41 -0300 Subject: [PATCH 12/13] fix: updating tests to coverage --- test/OriumSftMarketplace.test.ts | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/test/OriumSftMarketplace.test.ts b/test/OriumSftMarketplace.test.ts index c2b035e..7466d70 100644 --- a/test/OriumSftMarketplace.test.ts +++ b/test/OriumSftMarketplace.test.ts @@ -1393,6 +1393,65 @@ describe('OriumSftMarketplace', () => { .batchRevokeRole([1], [UNIQUE_ROLE], [borrower.address], [await wearableToken.getAddress()]), ).to.be.revertedWith('OriumSftMarketplace: role is not revocable Legacy') }) + it('Should NOT batch revoke role if if role is expired', async () => { + await rolesRegistry + .connect(lender) + .grantRole( + 1, + UNIQUE_ROLE, + borrower.address, + Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY, + false, + EMPTY_BYTES, + ) + await rolesRegistry + .connect(borrower) + .setRoleApprovalForAll(await mockERC1155.getAddress(), await marketplace.getAddress(), true) + await time.increase(ONE_DAY) + await expect( + marketplace + .connect(lender) + .batchRevokeRole([1], [UNIQUE_ROLE], [borrower.address], [await mockERC1155.getAddress()]), + ).to.be.revertedWith('OriumSftMarketplace: role is expired') + }) + it('Should NOT batch LEGACY revoke role if role is expired', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + commitAndGrantRoleParams[0].tokenAddress = await wearableToken.getAddress() + + await SftRolesRegistrySingleRoleLegacy.connect(lender).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + await wearableToken + .connect(lender) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + expect(marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams)) + + await SftRolesRegistrySingleRoleLegacy.connect(lender).grantRole( + 1, + UNIQUE_ROLE, + borrower.address, + Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY, + false, + EMPTY_BYTES, + ) + await SftRolesRegistrySingleRoleLegacy.connect(borrower).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + await time.increase(ONE_DAY) + await expect( + marketplace + .connect(lender) + .batchRevokeRole([1], [UNIQUE_ROLE], [borrower.address], [await wearableToken.getAddress()]), + ).to.be.revertedWith('OriumSftMarketplace: role is expired') + }) }) }) }) From 3bf755bc53c436e0fc8a28b4e939df6d6721fe41 Mon Sep 17 00:00:00 2001 From: EDUARDO MELO DE SIQUEIRA Date: Sat, 13 Jul 2024 16:45:26 -0300 Subject: [PATCH 13/13] fix: Perform to cover 100% test coverage --- test/OriumSftMarketplace.test.ts | 116 ++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 9 deletions(-) diff --git a/test/OriumSftMarketplace.test.ts b/test/OriumSftMarketplace.test.ts index 7466d70..1533daf 100644 --- a/test/OriumSftMarketplace.test.ts +++ b/test/OriumSftMarketplace.test.ts @@ -505,25 +505,23 @@ describe('OriumSftMarketplace', () => { await SftRolesRegistrySingleRoleLegacy.getAddress(), ) - await marketplace.connect(lender).createRentalOffer(rentalOfferLegacy) - await time.increase(ONE_DAY) - rentalOfferLegacy.commitmentId = BigInt(1) - rentalOfferLegacy.nonce = `0x${randomBytes(32).toString('hex')}` - rentalOfferLegacy.deadline = (await time.latest()) + ONE_DAY - const newTokenId = 2 await wearableToken.mint(lender.address, newTokenId, rentalOfferLegacy.tokenAmount, '0x') await wearableToken .connect(lender) .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) - await SftRolesRegistrySingleRoleLegacy.connect(lender).commitTokens( lender.address, rentalOfferLegacy.tokenAddress, newTokenId, rentalOfferLegacy.tokenAmount, ) - rentalOfferLegacy.commitmentId = BigInt(2) + + rentalOfferLegacy.commitmentId = BigInt(1) + rentalOfferLegacy.nonce = `0x${randomBytes(32).toString('hex')}` + rentalOfferLegacy.deadline = (await time.latest()) + ONE_DAY + + rentalOffer.commitmentId = BigInt(2) await expect(marketplace.connect(lender).createRentalOffer(rentalOfferLegacy)).to.be.revertedWith( "OriumSftMarketplace: tokenId provided does not match commitment's tokenId", ) @@ -538,7 +536,6 @@ describe('OriumSftMarketplace', () => { }) }) }) - describe('When Rental Offer is created', async () => { let totalFeeAmount: bigint beforeEach(async () => { @@ -1135,6 +1132,41 @@ describe('OriumSftMarketplace', () => { commitAndGrantRoleParams[0].data, ) }) + it('Should only grant Legacy role when a commitmentId is passed', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + commitAndGrantRoleParams[0].tokenAddress = await wearableToken.getAddress() + + await SftRolesRegistrySingleRoleLegacy.connect(lender).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + await wearableToken + .connect(lender) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + commitAndGrantRoleParams[0].commitmentId = BigInt(1) + await SftRolesRegistrySingleRoleLegacy.connect(lender).commitTokens( + lender.address, + await wearableToken.getAddress(), + tokenId, + tokenAmount, + ) + await expect(marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams)) + .to.emit(SftRolesRegistrySingleRoleLegacy, 'RoleGranted') + .withArgs( + 1, + commitAndGrantRoleParams[0].role, + commitAndGrantRoleParams[0].grantee, + commitAndGrantRoleParams[0].expirationDate, + commitAndGrantRoleParams[0].revocable, + commitAndGrantRoleParams[0].data, + ) + }) it('Should NOT commit tokens and grant role if contract is paused', async () => { await marketplace.connect(operator).pause() await expect( @@ -1173,6 +1205,34 @@ describe('OriumSftMarketplace', () => { marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams), ).to.revertedWith("OriumSftMarketplace: tokenId provided does not match commitment's tokenId") }) + it('Should NOT commit Legacy tokens and grant role if tokenId does not match the commitment', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + await SftRolesRegistrySingleRoleLegacy.connect(lender).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + await wearableToken + .connect(lender) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + commitAndGrantRoleParams[0].tokenAddress = await wearableToken.getAddress() + commitAndGrantRoleParams[0].commitmentId = BigInt(1) + commitAndGrantRoleParams[0].tokenId = 0 + await SftRolesRegistrySingleRoleLegacy.connect(lender).commitTokens( + lender.address, + await wearableToken.getAddress(), + tokenId, + tokenAmount, + ) + await expect( + marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams), + ).to.revertedWith("OriumSftMarketplace: tokenId provided does not match commitment's tokenId") + }) it('Should NOT commit tokens and grant role if tokenAmount does not match the commitment', async () => { await rolesRegistry.setTokenAddressAllowed(commitAndGrantRoleParams[0].tokenAddress, true) commitAndGrantRoleParams[0].commitmentId = BigInt(1) @@ -1278,6 +1338,32 @@ describe('OriumSftMarketplace', () => { .to.emit(rolesRegistry, 'RoleRevoked') .withArgs(1, UNIQUE_ROLE, borrower.address) }) + it("Should batch revoke Legacy role if sender is commitment's grantee", async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await wearableToken.getAddress(), await SftRolesRegistrySingleRoleLegacy.getAddress()) + await wearableToken.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + + commitAndGrantRoleParams[0].tokenAddress = await wearableToken.getAddress() + + await SftRolesRegistrySingleRoleLegacy.connect(lender).setRoleApprovalForAll( + await wearableToken.getAddress(), + await marketplace.getAddress(), + true, + ) + await wearableToken + .connect(lender) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) + expect(marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams)) + + await expect( + marketplace + .connect(borrower) + .batchRevokeRole([1], [UNIQUE_ROLE], [borrower.address], [await wearableToken.getAddress()]), + ) + .to.emit(SftRolesRegistrySingleRoleLegacy, 'RoleRevoked') + .withArgs(1, UNIQUE_ROLE, borrower.address) + }) it('Should use IERC7589Legacy if tokenAddress matches wearableAddress and Batch Revole Role ', async () => { await marketplaceRoyalties .connect(operator) @@ -1321,8 +1407,20 @@ describe('OriumSftMarketplace', () => { await marketplace.getAddress(), true, ) + await wearableToken + .connect(lender) + .setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true) expect(marketplace.connect(lender).batchCommitTokensAndGrantRole(commitAndGrantRoleParams)) + await SftRolesRegistrySingleRoleLegacy.connect(lender).grantRole( + 1, + UNIQUE_ROLE, + borrower.address, + Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY, + false, + EMPTY_BYTES, + ) + await expect( marketplace .connect(notOperator)