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

Commit 712e0d0

Browse files
authored
IERC721Roles & IERC721RolesManager (#17)
1 parent 6a24cc4 commit 712e0d0

File tree

3 files changed

+367
-0
lines changed

3 files changed

+367
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
pragma solidity ^0.8.0;
2+
3+
import "../../utils/Context.sol";
4+
import "./extensions/IERC721Roles.sol";
5+
import "../../utils/introspection/ERC165.sol";
6+
7+
contract ERC721RolesRentalAgreement is Context, IERC721RolesManager, ERC165 {
8+
// The status of the rent
9+
enum RentalStatus {
10+
pending,
11+
active,
12+
finished
13+
}
14+
15+
// A representation of rental agreement terms
16+
struct RentalAgreement {
17+
// The duration of the rent, in seconds
18+
uint32 rentalDuration;
19+
// The timestamp after which the rental agreement has expired and is no longer valid
20+
uint32 expirationDate;
21+
// The timestamp corresponding to the start of the rental period
22+
uint32 startTime;
23+
// The fees in wei that a renter needs to pay to start the rent
24+
uint256 rentalFees;
25+
RentalStatus rentalStatus;
26+
}
27+
28+
// The ERC721Roles token for which this contract can be used as a roles manager
29+
IERC721Roles public erc721Contract;
30+
31+
// Mapping from tokenId to rental agreement
32+
mapping(uint256 => RentalAgreement) public tokenIdToRentalAgreement;
33+
34+
// Mapping addresses to balances
35+
mapping(address => uint256) public balances;
36+
37+
// The identifier of the role Renter
38+
bytes4 public renterRoleId = bytes4(keccak256("ERC721Roles::Renter"));
39+
40+
constructor(IERC721Roles _erc721Contract) {
41+
erc721Contract = _erc721Contract;
42+
}
43+
44+
// ===== Modifiers ====== //
45+
modifier onlyErc721Contract() {
46+
require(
47+
_msgSender() == address(erc721Contract),
48+
"ERC721RolesRentalAgreement: only erc721Contract contract can modify state"
49+
);
50+
_;
51+
}
52+
53+
function afterRolesManagerRemoved(uint256 tokenId) external view onlyErc721Contract {
54+
RentalAgreement memory agreement = tokenIdToRentalAgreement[tokenId];
55+
require(
56+
agreement.rentalStatus != RentalStatus.active,
57+
"ERC721RolesRentalAgreement: can't remove the roles manager contract if there is an active rental"
58+
);
59+
}
60+
61+
// Allow the token's owner or operator to set up a new rental agreement.
62+
function setRentalAgreement(
63+
uint256 tokenId,
64+
uint32 duration,
65+
uint32 expirationDate,
66+
uint256 fees
67+
) public {
68+
require(
69+
_isOwnerOrApproved(tokenId, _msgSender()),
70+
"ERC721RolesRentalAgreement: only owner or approver can set up a rental agreement"
71+
);
72+
73+
RentalAgreement memory currentAgreement = tokenIdToRentalAgreement[tokenId];
74+
require(
75+
currentAgreement.rentalStatus != RentalStatus.active,
76+
"ERC721RolesRentalAgreement: can't update rental agreement if there is an active one already"
77+
);
78+
79+
RentalAgreement memory rentalAgreement = RentalAgreement(
80+
duration,
81+
expirationDate,
82+
0,
83+
fees,
84+
RentalStatus.pending
85+
);
86+
// Set the rental agreement
87+
tokenIdToRentalAgreement[tokenId] = rentalAgreement;
88+
}
89+
90+
// startRental allows an address to start the rent by paying the fees
91+
function startRental(address forAddress, uint256 tokenId) public payable {
92+
RentalAgreement memory rentalAgreement = tokenIdToRentalAgreement[tokenId];
93+
require(
94+
block.timestamp <= rentalAgreement.expirationDate,
95+
"ERC721RolesRentalAgreement: rental agreement expired"
96+
);
97+
require(
98+
rentalAgreement.rentalStatus != RentalStatus.active,
99+
"ERC721RolesRentalAgreement: rental already in progress"
100+
);
101+
102+
uint256 rentalFees = rentalAgreement.rentalFees;
103+
require(msg.value >= rentalFees, "ERC721RolesRentalAgreement: value below the rental fees");
104+
105+
address owner = erc721Contract.ownerOf(tokenId);
106+
// Credit the fees to the owner's balance
107+
balances[owner] += rentalFees;
108+
// Credit the remaining value to the sender's balance
109+
balances[_msgSender()] += msg.value - rentalFees;
110+
111+
// Start the rental
112+
rentalAgreement.rentalStatus = RentalStatus.active;
113+
rentalAgreement.startTime = uint32(block.timestamp);
114+
115+
// Reflect the role in the ERC721 token
116+
erc721Contract.grantRole(forAddress, tokenId, renterRoleId);
117+
}
118+
119+
// stopRental stops the rental
120+
function stopRental(address forAddress, uint256 tokenId) public {
121+
RentalAgreement memory rentalAgreement = tokenIdToRentalAgreement[tokenId];
122+
require(rentalAgreement.rentalStatus == RentalStatus.active, "ERC721RolesRentalAgreement: rental not active");
123+
require(
124+
block.timestamp - rentalAgreement.startTime >= rentalAgreement.rentalDuration,
125+
"ERC721RolesRentalAgreement: rental still ongoing"
126+
);
127+
128+
// Stop the rental
129+
rentalAgreement.rentalStatus = RentalStatus.finished;
130+
131+
// Revoke the renter's role
132+
erc721Contract.revokeRole(forAddress, tokenId, renterRoleId);
133+
}
134+
135+
// afterRoleGranted will be called back in `erc721Contract.grantRole`
136+
function afterRoleGranted(
137+
address fromAddress,
138+
address,
139+
uint256,
140+
bytes4
141+
) external view onlyErc721Contract {
142+
require(fromAddress == address(this), "ERC721RolesRentalAgreement: only this contract can set up renter roles");
143+
}
144+
145+
// afterRoleRevoked will be called back in `erc721Contract.revokeRole`
146+
function afterRoleRevoked(
147+
address fromAddress,
148+
address,
149+
uint256,
150+
bytes4
151+
) external view onlyErc721Contract {
152+
require(fromAddress == address(this), "ERC721RolesRentalAgreement: only this contract can revoke renter roles");
153+
}
154+
155+
function _isOwnerOrApproved(uint256 tokenId, address sender) internal view returns (bool) {
156+
address owner = erc721Contract.ownerOf(tokenId);
157+
return
158+
owner == sender ||
159+
erc721Contract.getApproved(tokenId) == sender ||
160+
erc721Contract.isApprovedForAll(owner, sender);
161+
}
162+
163+
// Allow addresses to redeem their funds
164+
function redeemFunds(uint256 _value) public {
165+
require(_value <= balances[_msgSender()], "ERC721RolesRentalAgreement: not enough funds to redeem");
166+
167+
balances[_msgSender()] -= _value;
168+
169+
// Check if the transfer is successful.
170+
require(_attemptETHTransfer(_msgSender(), _value), "ERC721RolesRentalAgreement: ETH transfer failed");
171+
}
172+
173+
function _attemptETHTransfer(address _to, uint256 _value) internal returns (bool) {
174+
// Here increase the gas limit a reasonable amount above the default, and try
175+
// to send ETH to the recipient.
176+
// NOTE: This might allow the recipient to attempt a limited reentrancy attack.
177+
(bool success, ) = _to.call{value: _value, gas: 30000}("");
178+
return success;
179+
}
180+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
pragma solidity ^0.8.0;
2+
3+
import "../ERC721.sol";
4+
import "./IERC721Roles.sol";
5+
6+
abstract contract ERC721Roles is ERC721, IERC721Roles {
7+
// Mapping from token ID to roles management contract
8+
mapping(uint256 => IERC721RolesManager) private _rolesManager;
9+
10+
// A record of the registered and active roles by tokenId
11+
mapping(uint256 => mapping(address => mapping(bytes4 => bool))) private _tokenIdRegisteredRoles;
12+
13+
/// @inheritdoc IERC721Roles
14+
function setRolesManager(uint256 tokenId, IERC721RolesManager tokenRolesManager) external {
15+
require(
16+
_isApprovedOrOwner(_msgSender(), tokenId),
17+
"ERC721Roles: only owner or approver can change roles manager"
18+
);
19+
20+
// If there is an existing roles manager contract, call `IERC721RolesManager.afterRolesManagerRemoved`
21+
IERC721RolesManager currentRolesManager = _rolesManager[tokenId];
22+
if (address(currentRolesManager) != address(0)) {
23+
currentRolesManager.afterRolesManagerRemoved(tokenId);
24+
}
25+
26+
// Update the roles manager contrac
27+
_rolesManager[tokenId] = tokenRolesManager;
28+
}
29+
30+
/// @inheritdoc IERC721Roles
31+
function rolesManager(uint256 tokenId) public view returns (IERC721RolesManager) {
32+
return _rolesManager[tokenId];
33+
}
34+
35+
/// @inheritdoc IERC721Roles
36+
function roleGranted(
37+
address user,
38+
uint256 tokenId,
39+
bytes4 roleId
40+
) external view returns (bool) {
41+
return _tokenIdRegisteredRoles[tokenId][user][roleId];
42+
}
43+
44+
/// @inheritdoc IERC721Roles
45+
function grantRole(
46+
address forAddress,
47+
uint256 tokenId,
48+
bytes4 roleId
49+
) external {
50+
IERC721RolesManager manager = _rolesManager[tokenId];
51+
require(address(manager) != address(0), "ERC721Roles: no roles manager set up");
52+
53+
// Register the new role
54+
_tokenIdRegisteredRoles[tokenId][forAddress][roleId] = true;
55+
56+
// Callback to the roles manager contract
57+
manager.afterRoleGranted(_msgSender(), forAddress, tokenId, roleId);
58+
}
59+
60+
/// @inheritdoc IERC721Roles
61+
function revokeRole(
62+
address forAddress,
63+
uint256 tokenId,
64+
bytes4 roleId
65+
) external {
66+
IERC721RolesManager manager = _rolesManager[tokenId];
67+
require(address(manager) != address(0), "ERC721Roles: no roles manager set up");
68+
69+
// De-register the role
70+
_tokenIdRegisteredRoles[tokenId][forAddress][roleId] = false;
71+
72+
// Callback to the roles manager contract
73+
manager.afterRoleRevoked(_msgSender(), forAddress, tokenId, roleId);
74+
}
75+
76+
/// @dev See {IERC165-supportsInterface}.
77+
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) {
78+
return interfaceId == type(IERC721Roles).interfaceId || super.supportsInterface(interfaceId);
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import "../IERC721.sol";
5+
import "../../../utils/introspection/IERC165.sol";
6+
7+
/// @title ERC721 token roles manager interface
8+
///
9+
/// Defines the interface that a roles manager contract should support to be used by
10+
/// `IERC721Roles`.
11+
interface IERC721RolesManager is IERC165 {
12+
/// Function called at the end of `IERC721Roles.setRolesManager` on the roles manager contract
13+
/// currently set for the token, if one exists.
14+
///
15+
/// @dev Allows the roles manager to cancel the change by reverting if it deems it
16+
/// necessary. The `IERC721Roles` is calling this function, so all information needed
17+
/// can be queried through the `msg.sender`.
18+
function afterRolesManagerRemoved(uint256 tokenId) external;
19+
20+
/// Function called at the end of `IERC721Roles.revokeRole`
21+
///
22+
/// @dev Allows the roles manager to cancel role withdrawal by reverting if it deems it
23+
/// necessary. The `IERC721Roles` is calling this function, so all information needed
24+
/// can be queried through the `msg.sender`.
25+
/// @param fromAddress The address that called `IERC721Roles.revokeRole`
26+
function afterRoleRevoked(
27+
address fromAddress,
28+
address forAddress,
29+
uint256 tokenId,
30+
bytes4 roleId
31+
) external;
32+
33+
/// Function called at the end of `IERC721Roles.grantRole`.
34+
///
35+
/// @dev Allows the roles manager to prevent adding a new role if it deems it
36+
/// necessary. The `IERC721Roles` is calling this function, so all information needed
37+
/// can be queried through the `msg.sender`.
38+
///
39+
/// @param fromAddress The address that called `IERC721Roles.grantRole`
40+
function afterRoleGranted(
41+
address fromAddress,
42+
address forAddress,
43+
uint256 tokenId,
44+
bytes4 roleId
45+
) external;
46+
}
47+
48+
/// @title ERC721 token roles interface
49+
///
50+
/// Defines the optional interface that allows setting roles for users by tokenId.
51+
/// It delegates the logic of adding and revoking roles to a roles manager contract implementing the
52+
/// `IERC721RolesManager` interface.
53+
/// A user could hold multiple roles and multiple users could be granted the same role. It's the
54+
/// responsability of the roles manager contract to allow such permissions.
55+
///
56+
/// Only the token's owner or an approver is able to change the roles manager contract,
57+
/// if authorized by the currently set RolesManager contract.
58+
///
59+
/// A role is defined similarly to functions' methodId by the first 4 bytes of its hash.
60+
/// For example, the renter role will be defined by bytes4(keccak256("ERC721Roles::Renter"))
61+
62+
interface IERC721Roles is IERC721 {
63+
/// Set the roles manager contract for a token.
64+
///
65+
/// A previously set roles manager contract must accept the change.
66+
/// The caller must be the token's owner or operator.
67+
///
68+
/// @dev If a roles manager contract was already set before this call, calls its
69+
/// `IERC721RolesManager.afterRolesManagerRemoved` at the end of the call.
70+
///
71+
/// @param rolesManager The roles manager contract. Set to 0 to remove the current roles manager.
72+
function setRolesManager(uint256 tokenId, IERC721RolesManager rolesManager) external;
73+
74+
/// @return the address of the roles manager, or 0 if there is no roles manager set.
75+
function rolesManager(uint256 tokenid) external returns (IERC721RolesManager);
76+
77+
/// @return true if the role has been granted to the user.
78+
function roleGranted(
79+
address user,
80+
uint256 tokenId,
81+
bytes4 roleId
82+
) external view returns (bool);
83+
84+
/// Set the role for the address and the token.
85+
///
86+
/// The token must have a roles manager contract set.
87+
/// The roles manager contract must accept the new role for the address
88+
///
89+
/// @dev Calls `IERC721RolesManager.afterRoleGranted` at the end of the call.
90+
function grantRole(
91+
address forAddress,
92+
uint256 tokenId,
93+
bytes4 roleId
94+
) external;
95+
96+
/// Revoke the role for the token and the address.
97+
///
98+
/// The token must have a roles manager contract set.
99+
/// The roles manager contract must accept the role withdrawal for this address.
100+
///
101+
/// @dev Calls `IERC721RolesManager.afterRoleRevoked` at the end of the call.
102+
function revokeRole(
103+
address forAddress,
104+
uint256 tokenId,
105+
bytes4 roleId
106+
) external;
107+
}

0 commit comments

Comments
 (0)