// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; import "./ERC5501.sol"; import "./IERC5501Terminable.sol"; /** * @dev Implementation of Terminable extension of https://eips.ethereum.org/EIPS/eip-5501 with OpenZeppelin ERC721 version. */ contract ERC5501Terminable is IERC5501Terminable, ERC5501 { /** * @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 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_) ERC5501(name_, symbol_) {} /** * @dev See {IERC5501-setUser}. */ function setUser( uint256 tokenId, address user, uint64 expires, bool isBorrowed ) public virtual override { super.setUser(tokenId, user, expires, isBorrowed); delete _borrowTerminations[tokenId]; emit ResetTerminationAgreements(tokenId); } /** * @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-getBorrowTermination}. */ function getBorrowTermination(uint256 tokenId) public view virtual override returns (bool, bool) { return ( _borrowTerminations[tokenId].lenderAgreement, _borrowTerminations[tokenId].borrowerAgreement ); } /** * @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 ); } /** * @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(IERC5501Terminable).interfaceId || super.supportsInterface(interfaceId); } /** * @dev Hook that is called after any token transfer. * If user is set and token is borrowed, reset termination agreements. */ function _afterTokenTransfer( address from, address to, uint256 tokenId ) internal virtual override { super._afterTokenTransfer(from, to, tokenId); if (from != to && _users[tokenId].isBorrowed) { delete _borrowTerminations[tokenId]; emit ResetTerminationAgreements(tokenId); } } }