diff --git a/contracts/OriumSftMarketplace.sol b/contracts/OriumSftMarketplace.sol index 44a1c13..41fb470 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 Aavegotchi Wearable address (legacy, only valid on Polygon) + address public constant aavegotchiWearableAddress = 0x58de9AaBCaeEC0f69883C94318810ad79Cc6a44f; + /// @dev hashedOffer => bool mapping(bytes32 => bool) public isCreated; @@ -128,9 +132,10 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra _validateCreateRentalOffer(_offer, _rolesRegistryAddress); if (_offer.commitmentId == 0) { - _offer.commitmentId = IERC7589(_rolesRegistryAddress).commitTokens( - _offer.lender, + _offer.commitmentId = _commitOrLockTokens( _offer.tokenAddress, + _rolesRegistryAddress, + _offer.lender, _offer.tokenId, _offer.tokenAmount ); @@ -198,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.releaseTokens(_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. @@ -242,10 +217,18 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra */ function delistRentalOfferAndWithdraw(RentalOffer calldata _offer) external whenNotPaused { _delistRentalOffer(_offer); - IERC7589 _rolesRegistry = IERC7589( - IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) - ); - _rolesRegistry.releaseTokens(_offer.commitmentId); + + 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); + } } /** @@ -301,12 +284,12 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra _params[i].tokenAddress ); - IERC7589 _rolesRegistry = IERC7589(_rolesRegistryAddress); uint256 _validCommitmentId = _params[i].commitmentId; if (_params[i].commitmentId == 0) { - _validCommitmentId = _rolesRegistry.commitTokens( - msg.sender, + _validCommitmentId = _commitOrLockTokens( _params[i].tokenAddress, + _rolesRegistryAddress, + msg.sender, _params[i].tokenId, _params[i].tokenAmount ); @@ -323,7 +306,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra ); } - _rolesRegistry.grantRole( + IERC7589(_rolesRegistryAddress).grantRole( _validCommitmentId, _params[i].role, _params[i].grantee, @@ -358,6 +341,29 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra } /** ######### Internals ########### **/ + + /** + * @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. + * @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 _offer The rental offer struct. @@ -490,6 +496,5 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra function setOriumMarketplaceRoyalties(address _oriumMarketplaceRoyalties) external onlyOwner { oriumMarketplaceRoyalties = _oriumMarketplaceRoyalties; } - /** ######### Getters ########### **/ } 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/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..7ec04e5 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( @@ -96,23 +100,39 @@ library LibOriumSftMarketplace { address _expectedGrantor, address _rolesRegistryAddress ) external view { - IERC7589 _rolesRegistry = IERC7589(_rolesRegistryAddress); - require( - _rolesRegistry.tokenAmountOf(_commitmentId) == _tokenAmount, - "OriumSftMarketplace: tokenAmount provided does not match commitment's tokenAmount" - ); - require( - _rolesRegistry.grantorOf(_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" - ); + if (_tokenAddress == aavegotchiWearableAddress) { + IERC7589Legacy _rolesRegistryLegacy = IERC7589Legacy(_rolesRegistryAddress); + 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 { + 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" + ); + } } /** @@ -120,19 +140,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 +178,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 +207,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).grantorOf(_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]); + + 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,30 +251,51 @@ 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], _grantees[i]), - "OriumSftMarketplace: role is not revocable" - ); - require( - IERC7589(_rolesRegistryAddress).roleExpirationDate(_commitmentIds[i], _roles[i], _grantees[i]) > block.timestamp, - "OriumSftMarketplace: role is expired" - ); - require( - msg.sender == _grantees[i] || IERC7589(_rolesRegistryAddress).grantorOf(_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" - ); + 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).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' + ); + } 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' + ); + 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 +310,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/SftRolesRegistrySingleRole.sol b/contracts/mocks/SftRolesRegistrySingleRole.sol index ade4ace..949b0a0 100644 --- a/contracts/mocks/SftRolesRegistrySingleRole.sol +++ b/contracts/mocks/SftRolesRegistrySingleRole.sol @@ -2,225 +2,266 @@ 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 { Uint64SortedLinkedListLibrary } from './Uint64SortedLinkedListLibrary.sol'; + + +contract SftRolesRegistrySingleRole is IERC7589, ERC1155Holder { + 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; + } + + address public managerAddress; + + address public marketplaceAddress; + + uint256 public lockIdCount; + + // tokenAddress => isAllowed + mapping(address => bool) public isTokenAddressAllowed; -// 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'); + // lockId => TokenLock + mapping(uint256 => TokenLock) public tokenLocks; - uint256 public commitmentCount; + // lockId => roleId => Role + mapping(uint256 => mapping(bytes32 => Role)) public roles; - // grantor => tokenAddress => operator => isApproved + // 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; + } - 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]; - } - 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' - ); + if (!_role.revocable) { + tokenLockExpirationDates[_lockId].remove(_role.expirationDate); + } - delete roleAssignments[_commitmentId][UNIQUE_ROLE]; + delete roles[_lockId][_roleId]; + emit RoleRevoked(_lockId, _roleId, _recipient); + } - _transferFrom( - address(this), - commitments[_commitmentId].grantor, - commitments[_commitmentId].tokenAddress, - commitments[_commitmentId].tokenId, - commitments[_commitmentId].tokenAmount - ); + function unlockTokens(uint256 _lockId) external onlyTokenLockOwnerOrApproved(_lockId) { + uint64 _headExpirationDate = tokenLockExpirationDates[_lockId].head; + require(_headExpirationDate < block.timestamp, 'ERC7589RolesRegistry: NFT is locked'); + + address _owner = tokenLocks[_lockId].owner; + address _tokenAddress = tokenLocks[_lockId].tokenAddress; + uint256 _tokenId = tokenLocks[_lockId].tokenId; + uint256 _tokenAmount = tokenLocks[_lockId].tokenAmount; + delete tokenLocks[_lockId]; + _transferFrom(address(this), _owner, _tokenAddress, _tokenId, _tokenAmount); + emit TokensUnlocked(_lockId); + } - delete commitments[_commitmentId]; - emit TokensReleased(_commitmentId); + function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _approved) external { + roleApprovals[msg.sender][_tokenAddress][_operator] = _approved; } - function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _isApproved) external override { - roleApprovals[msg.sender][_tokenAddress][_operator] = _isApproved; - emit RoleApprovalForAll(_tokenAddress, _operator, _isApproved); + /** Manager External Functions **/ + + function setMarketplaceAddress(address _marketplaceAddress) external onlyManager { + marketplaceAddress = _marketplaceAddress; } - /** Optional External Functions **/ + function setTokenAddressAllowed(address _tokenAddress, bool _isAllowed) external onlyManager { + isTokenAddressAllowed[_tokenAddress] = _isAllowed; + } - 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); + 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); + ) + 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); + _transferFrom(_owner, address(this), _tokenAddress, _tokenId, _tokenAmount); + emit TokensLocked(_owner, lockId_, _tokenAddress, _tokenId, _tokenAmount); } - function _grantOrUpdateRole( - uint256 _commitmentId, - bytes32 _role, - address _grantee, + 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 +275,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/SftRolesRegistrySingleRoleLegacy.sol b/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol new file mode 100644 index 0000000..9880a8b --- /dev/null +++ b/contracts/mocks/SftRolesRegistrySingleRoleLegacy.sol @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +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'; +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 IERC7589Legacy, ERC1155Holder { + 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); + } + + /** 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(IERC7589Legacy).interfaceId || + interfaceId == type(IERC1155Receiver).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/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..1533daf 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 () => { @@ -111,11 +126,22 @@ describe('OriumSftMarketplace', () => { rolesData: [EMPTY_BYTES], } + await rolesRegistry.setTokenAddressAllowed(rentalOffer.tokenAddress, true) + await mockERC1155.mint(lender.address, tokenId, tokenAmount, '0x') await rolesRegistry .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 () => { @@ -146,7 +172,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,39 +205,61 @@ 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 () { - rentalOffer.feeAmountPerSecond = BigInt(0) - rentalOffer.borrower = lender.address - await expect(marketplace.connect(lender).createRentalOffer(rentalOffer)) + 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( - rentalOffer.nonce, - rentalOffer.tokenAddress, - rentalOffer.tokenId, - rentalOffer.tokenAmount, + rentalOfferLegacy.nonce, + rentalOfferLegacy.tokenAddress, + rentalOfferLegacy.tokenId, + rentalOfferLegacy.tokenAmount, 1, - rentalOffer.lender, - rentalOffer.borrower, - rentalOffer.feeTokenAddress, - rentalOffer.feeAmountPerSecond, - rentalOffer.deadline, - rentalOffer.minDuration, - rentalOffer.roles, - rentalOffer.rolesData, + rentalOfferLegacy.lender, + rentalOfferLegacy.borrower, + rentalOfferLegacy.feeTokenAddress, + rentalOfferLegacy.feeAmountPerSecond, + rentalOfferLegacy.deadline, + rentalOfferLegacy.minDuration, + rentalOfferLegacy.roles, + rentalOfferLegacy.rolesData, ) - .to.emit(mockERC1155, 'TransferSingle') + .to.emit(wearableToken, 'TransferSingle') .withArgs( - await rolesRegistry.getAddress(), + await SftRolesRegistrySingleRoleLegacy.getAddress(), lender.address, - await rolesRegistry.getAddress(), + await SftRolesRegistrySingleRoleLegacy.getAddress(), tokenId, tokenAmount, ) - .to.emit(rolesRegistry, 'TokensCommitted') - .withArgs(lender.address, 1, await mockERC1155.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( @@ -300,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 @@ -337,7 +391,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,16 +399,58 @@ 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', ) }) + 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() 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 +462,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,12 +475,57 @@ 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", ) }) + 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(), + ) + + 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(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", + ) + }) 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) @@ -395,7 +536,6 @@ describe('OriumSftMarketplace', () => { }) }) }) - describe('When Rental Offer is created', async () => { let totalFeeAmount: bigint beforeEach(async () => { @@ -413,6 +553,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')}` @@ -587,62 +740,52 @@ 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, '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 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, 'TokensReleased') - .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).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 +823,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) @@ -720,13 +863,71 @@ 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]), ) - .to.emit(rolesRegistry, 'TokensReleased') + .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( @@ -808,7 +1009,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 () => { @@ -819,15 +1020,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, 'TokensReleased') - }) - }) - describe('Batch Release Tokens', async () => { it('Should release tokens after rental is ended and rental offer expired', async () => { await marketplace.connect(borrower).endRental(rentalOffer) @@ -837,7 +1029,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 +1037,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 +1068,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( @@ -889,11 +1083,44 @@ describe('OriumSftMarketplace', () => { commitAndGrantRoleParams[0].data, ) }) + it('Should use IERC7589Legacy if tokenAddress matches wearableAddress and Bach 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) + 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( @@ -905,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( @@ -912,40 +1174,101 @@ 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 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) 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") + }) + + 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") @@ -969,6 +1292,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) @@ -1014,6 +1338,55 @@ 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) + .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 @@ -1021,6 +1394,39 @@ 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, + ) + 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) + .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]), @@ -1048,6 +1454,102 @@ 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') + }) + 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') + }) }) }) }) 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..073b6e1 100644 --- a/test/fixtures/OriumSftMarketplaceFixture.ts +++ b/test/fixtures/OriumSftMarketplaceFixture.ts @@ -1,6 +1,12 @@ import { ethers, upgrades } from 'hardhat' import { AddressZero, THREE_MONTHS } from '../../utils/constants' -import { OriumMarketplaceRoyalties, OriumSftMarketplace, SftRolesRegistrySingleRole } from '../../typechain-types' +import { + OriumMarketplaceRoyalties, + OriumSftMarketplace, + SftRolesRegistrySingleRole, + MockERC1155, + SftRolesRegistrySingleRoleLegacy, +} from '../../typechain-types' /** * @dev deployer, operator needs to be the first accounts in the hardhat ethers.getSigners() @@ -10,10 +16,20 @@ 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 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, @@ -49,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 WearableFactory = await ethers.getContractFactory('MockERC1155') + const wearableToken = await WearableFactory.deploy() + await wearableToken.waitForDeployment() const secondMockERC1155 = await MockERC1155Factory.deploy() await secondMockERC1155.waitForDeployment() @@ -60,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, + wearableToken, + rolesRegistryLegacy, + ] as const }