diff --git a/contracts/token/ERC721/ERC721RolesRentalAgreement.sol b/contracts/token/ERC721/ERC721RolesRentalAgreement.sol new file mode 100644 index 00000000000..3c7bc531bae --- /dev/null +++ b/contracts/token/ERC721/ERC721RolesRentalAgreement.sol @@ -0,0 +1,180 @@ +pragma solidity ^0.8.0; + +import "../../utils/Context.sol"; +import "./extensions/IERC721Roles.sol"; +import "../../utils/introspection/ERC165.sol"; + +contract ERC721RolesRentalAgreement is Context, IERC721RolesManager, ERC165 { + // The status of the rent + enum RentalStatus { + pending, + active, + finished + } + + // A representation of rental agreement terms + struct RentalAgreement { + // The duration of the rent, in seconds + uint32 rentalDuration; + // The timestamp after which the rental agreement has expired and is no longer valid + uint32 expirationDate; + // The timestamp corresponding to the start of the rental period + uint32 startTime; + // The fees in wei that a renter needs to pay to start the rent + uint256 rentalFees; + RentalStatus rentalStatus; + } + + // The ERC721Roles token for which this contract can be used as a roles manager + IERC721Roles public erc721Contract; + + // Mapping from tokenId to rental agreement + mapping(uint256 => RentalAgreement) public tokenIdToRentalAgreement; + + // Mapping addresses to balances + mapping(address => uint256) public balances; + + // The identifier of the role Renter + bytes4 public renterRoleId = bytes4(keccak256("ERC721Roles::Renter")); + + constructor(IERC721Roles _erc721Contract) { + erc721Contract = _erc721Contract; + } + + // ===== Modifiers ====== // + modifier onlyErc721Contract() { + require( + _msgSender() == address(erc721Contract), + "ERC721RolesRentalAgreement: only erc721Contract contract can modify state" + ); + _; + } + + function afterRolesManagerRemoved(uint256 tokenId) external view onlyErc721Contract { + RentalAgreement memory agreement = tokenIdToRentalAgreement[tokenId]; + require( + agreement.rentalStatus != RentalStatus.active, + "ERC721RolesRentalAgreement: can't remove the roles manager contract if there is an active rental" + ); + } + + // Allow the token's owner or operator to set up a new rental agreement. + function setRentalAgreement( + uint256 tokenId, + uint32 duration, + uint32 expirationDate, + uint256 fees + ) public { + require( + _isOwnerOrApproved(tokenId, _msgSender()), + "ERC721RolesRentalAgreement: only owner or approver can set up a rental agreement" + ); + + RentalAgreement memory currentAgreement = tokenIdToRentalAgreement[tokenId]; + require( + currentAgreement.rentalStatus != RentalStatus.active, + "ERC721RolesRentalAgreement: can't update rental agreement if there is an active one already" + ); + + RentalAgreement memory rentalAgreement = RentalAgreement( + duration, + expirationDate, + 0, + fees, + RentalStatus.pending + ); + // Set the rental agreement + tokenIdToRentalAgreement[tokenId] = rentalAgreement; + } + + // startRental allows an address to start the rent by paying the fees + function startRental(address forAddress, uint256 tokenId) public payable { + RentalAgreement memory rentalAgreement = tokenIdToRentalAgreement[tokenId]; + require( + block.timestamp <= rentalAgreement.expirationDate, + "ERC721RolesRentalAgreement: rental agreement expired" + ); + require( + rentalAgreement.rentalStatus != RentalStatus.active, + "ERC721RolesRentalAgreement: rental already in progress" + ); + + uint256 rentalFees = rentalAgreement.rentalFees; + require(msg.value >= rentalFees, "ERC721RolesRentalAgreement: value below the rental fees"); + + address owner = erc721Contract.ownerOf(tokenId); + // Credit the fees to the owner's balance + balances[owner] += rentalFees; + // Credit the remaining value to the sender's balance + balances[_msgSender()] += msg.value - rentalFees; + + // Start the rental + rentalAgreement.rentalStatus = RentalStatus.active; + rentalAgreement.startTime = uint32(block.timestamp); + + // Reflect the role in the ERC721 token + erc721Contract.grantRole(forAddress, tokenId, renterRoleId); + } + + // stopRental stops the rental + function stopRental(address forAddress, uint256 tokenId) public { + RentalAgreement memory rentalAgreement = tokenIdToRentalAgreement[tokenId]; + require(rentalAgreement.rentalStatus == RentalStatus.active, "ERC721RolesRentalAgreement: rental not active"); + require( + block.timestamp - rentalAgreement.startTime >= rentalAgreement.rentalDuration, + "ERC721RolesRentalAgreement: rental still ongoing" + ); + + // Stop the rental + rentalAgreement.rentalStatus = RentalStatus.finished; + + // Revoke the renter's role + erc721Contract.revokeRole(forAddress, tokenId, renterRoleId); + } + + // afterRoleGranted will be called back in `erc721Contract.grantRole` + function afterRoleGranted( + address fromAddress, + address, + uint256, + bytes4 + ) external view onlyErc721Contract { + require(fromAddress == address(this), "ERC721RolesRentalAgreement: only this contract can set up renter roles"); + } + + // afterRoleRevoked will be called back in `erc721Contract.revokeRole` + function afterRoleRevoked( + address fromAddress, + address, + uint256, + bytes4 + ) external view onlyErc721Contract { + require(fromAddress == address(this), "ERC721RolesRentalAgreement: only this contract can revoke renter roles"); + } + + function _isOwnerOrApproved(uint256 tokenId, address sender) internal view returns (bool) { + address owner = erc721Contract.ownerOf(tokenId); + return + owner == sender || + erc721Contract.getApproved(tokenId) == sender || + erc721Contract.isApprovedForAll(owner, sender); + } + + // Allow addresses to redeem their funds + function redeemFunds(uint256 _value) public { + require(_value <= balances[_msgSender()], "ERC721RolesRentalAgreement: not enough funds to redeem"); + + balances[_msgSender()] -= _value; + + // Check if the transfer is successful. + require(_attemptETHTransfer(_msgSender(), _value), "ERC721RolesRentalAgreement: ETH transfer failed"); + } + + function _attemptETHTransfer(address _to, uint256 _value) internal returns (bool) { + // Here increase the gas limit a reasonable amount above the default, and try + // to send ETH to the recipient. + // NOTE: This might allow the recipient to attempt a limited reentrancy attack. + (bool success, ) = _to.call{value: _value, gas: 30000}(""); + return success; + } +} diff --git a/contracts/token/ERC721/extensions/ERC721Roles.sol b/contracts/token/ERC721/extensions/ERC721Roles.sol new file mode 100644 index 00000000000..97098ec5325 --- /dev/null +++ b/contracts/token/ERC721/extensions/ERC721Roles.sol @@ -0,0 +1,80 @@ +pragma solidity ^0.8.0; + +import "../ERC721.sol"; +import "./IERC721Roles.sol"; + +abstract contract ERC721Roles is ERC721, IERC721Roles { + // Mapping from token ID to roles management contract + mapping(uint256 => IERC721RolesManager) private _rolesManager; + + // A record of the registered and active roles by tokenId + mapping(uint256 => mapping(address => mapping(bytes4 => bool))) private _tokenIdRegisteredRoles; + + /// @inheritdoc IERC721Roles + function setRolesManager(uint256 tokenId, IERC721RolesManager tokenRolesManager) external { + require( + _isApprovedOrOwner(_msgSender(), tokenId), + "ERC721Roles: only owner or approver can change roles manager" + ); + + // If there is an existing roles manager contract, call `IERC721RolesManager.afterRolesManagerRemoved` + IERC721RolesManager currentRolesManager = _rolesManager[tokenId]; + if (address(currentRolesManager) != address(0)) { + currentRolesManager.afterRolesManagerRemoved(tokenId); + } + + // Update the roles manager contrac + _rolesManager[tokenId] = tokenRolesManager; + } + + /// @inheritdoc IERC721Roles + function rolesManager(uint256 tokenId) public view returns (IERC721RolesManager) { + return _rolesManager[tokenId]; + } + + /// @inheritdoc IERC721Roles + function roleGranted( + address user, + uint256 tokenId, + bytes4 roleId + ) external view returns (bool) { + return _tokenIdRegisteredRoles[tokenId][user][roleId]; + } + + /// @inheritdoc IERC721Roles + function grantRole( + address forAddress, + uint256 tokenId, + bytes4 roleId + ) external { + IERC721RolesManager manager = _rolesManager[tokenId]; + require(address(manager) != address(0), "ERC721Roles: no roles manager set up"); + + // Register the new role + _tokenIdRegisteredRoles[tokenId][forAddress][roleId] = true; + + // Callback to the roles manager contract + manager.afterRoleGranted(_msgSender(), forAddress, tokenId, roleId); + } + + /// @inheritdoc IERC721Roles + function revokeRole( + address forAddress, + uint256 tokenId, + bytes4 roleId + ) external { + IERC721RolesManager manager = _rolesManager[tokenId]; + require(address(manager) != address(0), "ERC721Roles: no roles manager set up"); + + // De-register the role + _tokenIdRegisteredRoles[tokenId][forAddress][roleId] = false; + + // Callback to the roles manager contract + manager.afterRoleRevoked(_msgSender(), forAddress, tokenId, roleId); + } + + /// @dev See {IERC165-supportsInterface}. + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) { + return interfaceId == type(IERC721Roles).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/contracts/token/ERC721/extensions/IERC721Roles.sol b/contracts/token/ERC721/extensions/IERC721Roles.sol new file mode 100644 index 00000000000..25a441d7430 --- /dev/null +++ b/contracts/token/ERC721/extensions/IERC721Roles.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../IERC721.sol"; +import "../../../utils/introspection/IERC165.sol"; + +/// @title ERC721 token roles manager interface +/// +/// Defines the interface that a roles manager contract should support to be used by +/// `IERC721Roles`. +interface IERC721RolesManager is IERC165 { + /// Function called at the end of `IERC721Roles.setRolesManager` on the roles manager contract + /// currently set for the token, if one exists. + /// + /// @dev Allows the roles manager to cancel the change by reverting if it deems it + /// necessary. The `IERC721Roles` is calling this function, so all information needed + /// can be queried through the `msg.sender`. + function afterRolesManagerRemoved(uint256 tokenId) external; + + /// Function called at the end of `IERC721Roles.revokeRole` + /// + /// @dev Allows the roles manager to cancel role withdrawal by reverting if it deems it + /// necessary. The `IERC721Roles` is calling this function, so all information needed + /// can be queried through the `msg.sender`. + /// @param fromAddress The address that called `IERC721Roles.revokeRole` + function afterRoleRevoked( + address fromAddress, + address forAddress, + uint256 tokenId, + bytes4 roleId + ) external; + + /// Function called at the end of `IERC721Roles.grantRole`. + /// + /// @dev Allows the roles manager to prevent adding a new role if it deems it + /// necessary. The `IERC721Roles` is calling this function, so all information needed + /// can be queried through the `msg.sender`. + /// + /// @param fromAddress The address that called `IERC721Roles.grantRole` + function afterRoleGranted( + address fromAddress, + address forAddress, + uint256 tokenId, + bytes4 roleId + ) external; +} + +/// @title ERC721 token roles interface +/// +/// Defines the optional interface that allows setting roles for users by tokenId. +/// It delegates the logic of adding and revoking roles to a roles manager contract implementing the +/// `IERC721RolesManager` interface. +/// A user could hold multiple roles and multiple users could be granted the same role. It's the +/// responsability of the roles manager contract to allow such permissions. +/// +/// Only the token's owner or an approver is able to change the roles manager contract, +/// if authorized by the currently set RolesManager contract. +/// +/// A role is defined similarly to functions' methodId by the first 4 bytes of its hash. +/// For example, the renter role will be defined by bytes4(keccak256("ERC721Roles::Renter")) + +interface IERC721Roles is IERC721 { + /// Set the roles manager contract for a token. + /// + /// A previously set roles manager contract must accept the change. + /// The caller must be the token's owner or operator. + /// + /// @dev If a roles manager contract was already set before this call, calls its + /// `IERC721RolesManager.afterRolesManagerRemoved` at the end of the call. + /// + /// @param rolesManager The roles manager contract. Set to 0 to remove the current roles manager. + function setRolesManager(uint256 tokenId, IERC721RolesManager rolesManager) external; + + /// @return the address of the roles manager, or 0 if there is no roles manager set. + function rolesManager(uint256 tokenid) external returns (IERC721RolesManager); + + /// @return true if the role has been granted to the user. + function roleGranted( + address user, + uint256 tokenId, + bytes4 roleId + ) external view returns (bool); + + /// Set the role for the address and the token. + /// + /// The token must have a roles manager contract set. + /// The roles manager contract must accept the new role for the address + /// + /// @dev Calls `IERC721RolesManager.afterRoleGranted` at the end of the call. + function grantRole( + address forAddress, + uint256 tokenId, + bytes4 roleId + ) external; + + /// Revoke the role for the token and the address. + /// + /// The token must have a roles manager contract set. + /// The roles manager contract must accept the role withdrawal for this address. + /// + /// @dev Calls `IERC721RolesManager.afterRoleRevoked` at the end of the call. + function revokeRole( + address forAddress, + uint256 tokenId, + bytes4 roleId + ) external; +}