Skip to content
This repository was archived by the owner on Apr 24, 2025. It is now read-only.

IERC721Roles & IERC721RolesManager #17

Merged
merged 4 commits into from
May 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions contracts/token/ERC721/ERC721RolesRentalAgreement.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
80 changes: 80 additions & 0 deletions contracts/token/ERC721/extensions/ERC721Roles.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
107 changes: 107 additions & 0 deletions contracts/token/ERC721/extensions/IERC721Roles.sol
Original file line number Diff line number Diff line change
@@ -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;
}