326 lines
9.3 KiB
Solidity
326 lines
9.3 KiB
Solidity
// SPDX-License-Identifier: CC0-1.0
|
|
|
|
pragma solidity ^0.8.0;
|
|
|
|
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
|
import "./IERC5501.sol";
|
|
import "./IERC5501Balance.sol";
|
|
import "./IERC5501Enumerable.sol";
|
|
import "./IERC5501Terminable.sol";
|
|
|
|
/**
|
|
* @dev Implementation of ERC5501 contract with all extensions https://eips.ethereum.org/EIPS/eip-5501 with OpenZeppelin ERC721 version.
|
|
*/
|
|
contract ERC5501Combined is
|
|
IERC5501,
|
|
IERC5501Balance,
|
|
IERC5501Terminable,
|
|
IERC5501Enumerable,
|
|
ERC721
|
|
{
|
|
/**
|
|
* @dev Structure to hold user information.
|
|
* @notice If isBorrowed is true, UserInfo cannot be modified before it expires.
|
|
*/
|
|
struct UserInfo {
|
|
address user; // Address of user role
|
|
uint64 expires; // Unix timestamp, user expires on
|
|
bool isBorrowed; // Borrowed flag
|
|
}
|
|
|
|
/**
|
|
* @dev Structure to hold agreements from both parties to terminate a borrow.
|
|
* @notice If both parties agree, it is possible to modify UserInfo even before it expires.
|
|
* In such case, isBorrowed status is reverted to false.
|
|
*/
|
|
struct BorrowTerminationInfo {
|
|
bool lenderAgreement;
|
|
bool borrowerAgreement;
|
|
}
|
|
|
|
// Mapping from token ID to UserInfo
|
|
mapping(uint256 => UserInfo) internal _users;
|
|
|
|
// Mapping from address to userOf tokens
|
|
mapping(address => uint256[]) internal _userBalances;
|
|
|
|
// Mapping from token ID to BorrowTerminationInfo
|
|
mapping(uint256 => BorrowTerminationInfo) internal _borrowTerminations;
|
|
|
|
/**
|
|
* @dev Initializes the contract by setting a name and a symbol to the token collection.
|
|
*/
|
|
constructor(string memory name_, string memory symbol_)
|
|
ERC721(name_, symbol_)
|
|
{}
|
|
|
|
/**
|
|
* @dev See {IERC5501-setUser}.
|
|
*/
|
|
function setUser(
|
|
uint256 tokenId,
|
|
address user,
|
|
uint64 expires,
|
|
bool isBorrowed
|
|
) public virtual override {
|
|
// Balance extension
|
|
flushExpired(user);
|
|
|
|
require(
|
|
_isApprovedOrOwner(msg.sender, tokenId),
|
|
"ERC5501: set user caller is not token owner or approved"
|
|
);
|
|
require(user != address(0), "ERC5501: set user to zero address");
|
|
|
|
UserInfo storage info = _users[tokenId];
|
|
require(
|
|
!info.isBorrowed || info.expires < block.timestamp,
|
|
"ERC5501: token is borrowed"
|
|
);
|
|
info.user = user;
|
|
info.expires = expires;
|
|
info.isBorrowed = isBorrowed;
|
|
emit UpdateUser(tokenId, user, expires, isBorrowed);
|
|
|
|
// Balance extension
|
|
_userBalances[user].push(tokenId);
|
|
// Terminable extension
|
|
delete _borrowTerminations[tokenId];
|
|
emit ResetTerminationAgreements(tokenId);
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC5501-userOf}.
|
|
*/
|
|
function userOf(uint256 tokenId)
|
|
public
|
|
view
|
|
virtual
|
|
override
|
|
returns (address)
|
|
{
|
|
require(
|
|
uint256(_users[tokenId].expires) >= block.timestamp,
|
|
"ERC5501: user does not exist for this token"
|
|
);
|
|
return _users[tokenId].user;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC5501-userBalanceOf}.
|
|
*/
|
|
function userBalanceOf(address user)
|
|
public
|
|
view
|
|
virtual
|
|
override
|
|
returns (uint256)
|
|
{
|
|
require(
|
|
user != address(0),
|
|
"ERC5501Balance: address zero is not a valid owner"
|
|
);
|
|
uint256 balance;
|
|
uint256[] memory candidates = _userBalances[user];
|
|
unchecked {
|
|
for (uint256 i; i < candidates.length; ++i) {
|
|
if (
|
|
_users[candidates[i]].expires >= block.timestamp &&
|
|
_users[candidates[i]].user == user
|
|
) {
|
|
++balance;
|
|
}
|
|
}
|
|
}
|
|
return balance;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC5501-tokenOfUserByIndex}.
|
|
*/
|
|
function tokenOfUserByIndex(address user, uint256 index)
|
|
public
|
|
view
|
|
virtual
|
|
override
|
|
returns (uint256)
|
|
{
|
|
require(
|
|
user != address(0),
|
|
"ERC5501Enumerable: address zero is not a valid owner"
|
|
);
|
|
uint256[] memory balance = _userBalances[user];
|
|
require(
|
|
balance.length > 0 && index < balance.length,
|
|
"ERC5501Enumerable: owner index out of bounds"
|
|
);
|
|
uint256 counter;
|
|
unchecked {
|
|
for (uint256 i; i < balance.length; ++i) {
|
|
if (
|
|
_users[balance[i]].expires >= block.timestamp &&
|
|
_users[balance[i]].user == user
|
|
) {
|
|
if (counter == index) {
|
|
return balance[i];
|
|
}
|
|
++counter;
|
|
}
|
|
}
|
|
}
|
|
revert("ERC5501Enumerable: owner index out of bounds");
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC5501-userExpires}.
|
|
*/
|
|
function userExpires(uint256 tokenId)
|
|
public
|
|
view
|
|
virtual
|
|
override
|
|
returns (uint64)
|
|
{
|
|
return _users[tokenId].expires;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC5501-isBorrowed}.
|
|
*/
|
|
function userIsBorrowed(uint256 tokenId)
|
|
public
|
|
view
|
|
virtual
|
|
override
|
|
returns (bool)
|
|
{
|
|
return _users[tokenId].isBorrowed;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC5501Terminable-getBorrowTermination}.
|
|
*/
|
|
function getBorrowTermination(uint256 tokenId)
|
|
public
|
|
view
|
|
virtual
|
|
override
|
|
returns (bool, bool)
|
|
{
|
|
return (
|
|
_borrowTerminations[tokenId].lenderAgreement,
|
|
_borrowTerminations[tokenId].borrowerAgreement
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC5501Terminable-setBorrowTermination}.
|
|
*/
|
|
function setBorrowTermination(uint256 tokenId) public virtual override {
|
|
UserInfo storage userInfo = _users[tokenId];
|
|
require(
|
|
userInfo.expires >= block.timestamp && userInfo.isBorrowed,
|
|
"ERC5501Terminable: borrow not active"
|
|
);
|
|
|
|
BorrowTerminationInfo storage terminationInfo = _borrowTerminations[
|
|
tokenId
|
|
];
|
|
if (ownerOf(tokenId) == msg.sender) {
|
|
terminationInfo.lenderAgreement = true;
|
|
emit AgreeToTerminateBorrow(tokenId, msg.sender, true);
|
|
}
|
|
if (userInfo.user == msg.sender) {
|
|
terminationInfo.borrowerAgreement = true;
|
|
emit AgreeToTerminateBorrow(tokenId, msg.sender, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC5501Terminable-terminateBorrow}.
|
|
*/
|
|
function terminateBorrow(uint256 tokenId) public virtual override {
|
|
BorrowTerminationInfo storage info = _borrowTerminations[tokenId];
|
|
require(
|
|
info.lenderAgreement && info.borrowerAgreement,
|
|
"ERC5501Terminable: not agreed"
|
|
);
|
|
_users[tokenId].isBorrowed = false;
|
|
delete _borrowTerminations[tokenId];
|
|
emit ResetTerminationAgreements(tokenId);
|
|
emit TerminateBorrow(
|
|
tokenId,
|
|
ownerOf(tokenId),
|
|
_users[tokenId].user,
|
|
msg.sender
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @notice On setUser flush all expired userOf statuses.
|
|
* @dev This function may revert out of gas if user borrows too many tokens at once.
|
|
* There must be a way to prevent such behaviour (such as flushing by parts only).
|
|
* @param user an address to flush
|
|
*/
|
|
function flushExpired(address user) internal {
|
|
uint256[] storage candidates = _userBalances[user];
|
|
unchecked {
|
|
for (uint256 i; i < candidates.length; ++i) {
|
|
if (
|
|
_users[candidates[i]].user != user ||
|
|
_users[candidates[i]].expires < block.timestamp
|
|
) {
|
|
candidates[i] = candidates[candidates.length - 1];
|
|
candidates.pop();
|
|
--i; // test moved element
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dev See {EIP-165: Standard Interface Detection}.
|
|
* https://eips.ethereum.org/EIPS/eip-165
|
|
*/
|
|
function supportsInterface(bytes4 interfaceId)
|
|
public
|
|
view
|
|
virtual
|
|
override
|
|
returns (bool)
|
|
{
|
|
return
|
|
interfaceId == type(IERC5501).interfaceId ||
|
|
interfaceId == type(IERC5501Balance).interfaceId ||
|
|
interfaceId == type(IERC5501Enumerable).interfaceId ||
|
|
interfaceId == type(IERC5501Terminable).interfaceId ||
|
|
super.supportsInterface(interfaceId);
|
|
}
|
|
|
|
/**
|
|
* @dev Hook that is called after any token transfer.
|
|
* If user is set and token is not borrowed, reset user.
|
|
*/
|
|
function _afterTokenTransfer(
|
|
address from,
|
|
address to,
|
|
uint256 tokenId
|
|
) internal virtual override {
|
|
super._afterTokenTransfer(from, to, tokenId);
|
|
if (
|
|
from != to &&
|
|
!_users[tokenId].isBorrowed &&
|
|
_users[tokenId].user != address(0)
|
|
) {
|
|
delete _users[tokenId];
|
|
emit UpdateUser(tokenId, address(0), 0, false);
|
|
} else if (
|
|
// Terminable extension
|
|
from != to && _users[tokenId].isBorrowed
|
|
) {
|
|
delete _borrowTerminations[tokenId];
|
|
emit ResetTerminationAgreements(tokenId);
|
|
}
|
|
}
|
|
}
|