From 78df433ad41842266c9fbae91d9b3b2dc52b9487 Mon Sep 17 00:00:00 2001 From: CanvasL <746591811@qq.com> Date: Tue, 30 Jul 2024 17:15:10 +0800 Subject: [PATCH] feat: change rental product to access token --- src/AccessToken.sol | 33 +++++++ src/AccessTokenFactory.sol | 27 ++++++ src/Marketplace.sol | 152 +++++++------------------------- src/RentalProduct.sol | 76 ---------------- src/RentalProductFactory.sol | 23 ----- src/interfaces/IMarketplace.sol | 88 ++++++++++++++++++ 6 files changed, 182 insertions(+), 217 deletions(-) create mode 100644 src/AccessToken.sol create mode 100644 src/AccessTokenFactory.sol delete mode 100644 src/RentalProduct.sol delete mode 100644 src/RentalProductFactory.sol create mode 100644 src/interfaces/IMarketplace.sol diff --git a/src/AccessToken.sol b/src/AccessToken.sol new file mode 100644 index 0000000..c6caa0c --- /dev/null +++ b/src/AccessToken.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {IProduct} from "./interfaces/IProduct.sol"; + +contract AccessToken is ERC721 { + IProduct public immutable PRODUCT; + + constructor( + IProduct product, + string memory name, + string memory symbol + ) ERC721(name, symbol) { + PRODUCT = product; + } + + modifier onlyTokenOwner(uint256 tokenId) { + require(msg.sender == PRODUCT.ownerOf(tokenId), "not token owner"); + _; + } + + function mint( + address user, + uint256 tokenId + ) external onlyTokenOwner(tokenId) { + _mint(user, tokenId); + } + + function burn(uint256 tokenId) external onlyTokenOwner(tokenId) { + _burn(tokenId); + } +} diff --git a/src/AccessTokenFactory.sol b/src/AccessTokenFactory.sol new file mode 100644 index 0000000..9a0ea99 --- /dev/null +++ b/src/AccessTokenFactory.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {IProduct} from "./interfaces/IProduct.sol"; +import {AccessToken} from "./AccessToken.sol"; + +contract AccessTokenFactory { + mapping(address => address) public getAccessToken; + + function createAccessToken( + address productAddress + ) external returns (address) { + require( + address(getAccessToken[productAddress]) == address(0), + "existing access token" + ); + + AccessToken accessToken = new AccessToken( + IProduct(productAddress), + ERC721(productAddress).name(), + ERC721(productAddress).symbol() + ); + getAccessToken[productAddress] = address(accessToken); + return address(accessToken); + } +} diff --git a/src/Marketplace.sol b/src/Marketplace.sol index 6d265d7..97cf520 100644 --- a/src/Marketplace.sol +++ b/src/Marketplace.sol @@ -4,102 +4,19 @@ pragma solidity ^0.8.24; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {RentalProductFactory} from "./RentalProductFactory.sol"; -import {RentalProduct} from "./RentalProduct.sol"; +import {AccessTokenFactory} from "./AccessTokenFactory.sol"; +import {AccessToken} from "./AccessToken.sol"; import {IProduct} from "./interfaces/IProduct.sol"; +import {IMarketplace} from "./interfaces/IMarketplace.sol"; -contract Marketplace is Ownable { - // Statuses of a listing. WithdrawnOrNotExist, which is 0, is effectively the same as never listed before. - enum ListingStatus { - WithdrawnOrNotExist, - Listing, - Delisted - } - - // Statuses of a rental. EndedOrNotExist, which is 0, is effectively the same as never exist before. - enum RentalStatus { - EndedOrNotExist, - Renting - } - - struct ListingInfo { - address owner; - uint256 minRentalDays; - uint256 maxRentalDays; - address rentCurrency; - uint256 dailyRent; - address payable rentRecipient; - ListingStatus status; - } - - struct RentalInfo { - uint256 startTime; - uint256 endTime; - uint256 rentalDays; - address rentCurrency; - uint256 dailyRent; - uint256 totalPaidRent; - RentalStatus status; - } - - struct ListArgs { - address product; - uint256 tokenId; - uint256 minRentalDays; - uint256 maxRentalDays; - address rentCurrency; - uint256 dailyRent; - address rentRecipient; - } - - struct DelistArgs { - address payable rentalProduct; - uint256 tokenId; - } - - struct RelistArgs { - address payable rentalProduct; - uint256 tokenId; - uint256 minRentalDays; - uint256 maxRentalDays; - address rentCurrency; - uint256 dailyRent; - address payable rentRecipient; - } - - struct RentArgs { - address payable rentalProduct; - uint256 tokenId; - address tenant; - uint256 rentalDays; - uint256 prepaidRent; - } - - struct PayRentArgs { - address payable rentalProduct; - uint256 tokenId; - address tenant; - uint256 rent; - } - - struct EndLeaseArgs { - address payable rentalProduct; - uint256 tokenId; - address tenant; - } - - struct WithdrawArgs { - address payable rentalProduct; - uint256 tokenId; - } - +contract Marketplace is IMarketplace, Ownable { using SafeERC20 for IERC20; - address private constant NATIVE_TOKEN = address(1); + address private constant NATIVE_TOKEN = address(0); uint256 public constant MAX_POINTS = 10000; - RentalProductFactory public immutable RENTAL_PRODUCT_FACTORY; + AccessTokenFactory public immutable ACCESS_TOKEN_FACTORY; address payable private _treasury; @@ -111,24 +28,24 @@ contract Marketplace is Ownable { mapping(address => bool) public supportedRentCurrencies; /** - * @notice rental product => token id => listing info + * @notice access token => token id => listing info */ mapping(address => mapping(uint256 => ListingInfo)) public listings; /** - * @notice rental product => token id => tenant => rent info + * @notice access token => token id => tenant => rent info */ mapping(address => mapping(uint256 => mapping(address => RentalInfo))) public rentals; constructor( address initialOwner, - address rentalProductFactory, + address accessTokenFactory, address[] memory rentCurrencies, address payable treasury, uint256 feePoints ) Ownable(initialOwner) { - RENTAL_PRODUCT_FACTORY = RentalProductFactory(rentalProductFactory); + ACCESS_TOKEN_FACTORY = AccessTokenFactory(accessTokenFactory); for (uint i = 0; i < rentCurrencies.length; i++) { supportedRentCurrencies[rentCurrencies[i]] = true; } @@ -161,14 +78,14 @@ contract Marketplace is Ownable { } function list(ListArgs memory args) public { - address rentalProduct = RENTAL_PRODUCT_FACTORY.getRentalProduct( + address accessToken = ACCESS_TOKEN_FACTORY.getAccessToken( args.product ); - if(rentalProduct == address(0)) { - rentalProduct = RENTAL_PRODUCT_FACTORY.createRentalProduct(args.product); + if(accessToken == address(0)) { + accessToken = ACCESS_TOKEN_FACTORY.createAccessToken(args.product); } require( - listings[rentalProduct][args.tokenId].status == + listings[accessToken][args.tokenId].status == ListingStatus.WithdrawnOrNotExist, // Never listed or withdrawn "token already listed" ); @@ -182,7 +99,7 @@ contract Marketplace is Ownable { "unsupported rent currency" ); - listings[rentalProduct][args.tokenId] = ListingInfo({ + listings[accessToken][args.tokenId] = ListingInfo({ owner: msg.sender, minRentalDays: args.minRentalDays, maxRentalDays: args.maxRentalDays, @@ -200,7 +117,7 @@ contract Marketplace is Ownable { } function delist(DelistArgs memory args) public { - ListingInfo storage listing = listings[args.rentalProduct][ + ListingInfo storage listing = listings[args.accessToken][ args.tokenId ]; require(listing.owner == msg.sender, "not listing owner"); @@ -208,7 +125,7 @@ contract Marketplace is Ownable { } function relist(RelistArgs memory args) public { - ListingInfo storage listing = listings[args.rentalProduct][ + ListingInfo storage listing = listings[args.accessToken][ args.tokenId ]; require(listing.owner == msg.sender, "not listing owner"); @@ -232,12 +149,12 @@ contract Marketplace is Ownable { function rent(RentArgs memory args) public payable { require( - rentals[args.rentalProduct][args.tokenId][args.tenant].status == + rentals[args.accessToken][args.tokenId][args.tenant].status == RentalStatus.EndedOrNotExist, "existing rental" ); - ListingInfo memory listing = listings[args.rentalProduct][args.tokenId]; + ListingInfo memory listing = listings[args.accessToken][args.tokenId]; require( listing.minRentalDays <= args.rentalDays && args.rentalDays <= listing.maxRentalDays, @@ -248,7 +165,7 @@ contract Marketplace is Ownable { "insufficient prepaid rent" ); - rentals[args.rentalProduct][args.tokenId][args.tenant] = RentalInfo({ + rentals[args.accessToken][args.tokenId][args.tenant] = RentalInfo({ startTime: block.timestamp, endTime: block.timestamp + args.rentalDays * 1 days, rentalDays: args.rentalDays, @@ -261,19 +178,19 @@ contract Marketplace is Ownable { // Pay rent _payRent( listing, - rentals[args.rentalProduct][args.tokenId][args.tenant], + rentals[args.accessToken][args.tokenId][args.tenant], args.prepaidRent ); - // Add the tenant to the rental token - RentalProduct(payable(args.rentalProduct)).addUser( - args.tokenId, - args.tenant + // Mint access token to tenant + AccessToken(args.accessToken).mint( + args.tenant, + args.tokenId ); } function payRent(PayRentArgs memory args) public payable { - ListingInfo memory listing = listings[args.rentalProduct][args.tokenId]; - RentalInfo storage rental = rentals[args.rentalProduct][args.tokenId][ + ListingInfo memory listing = listings[args.accessToken][args.tokenId]; + RentalInfo storage rental = rentals[args.accessToken][args.tokenId][ args.tenant ]; require( @@ -287,7 +204,7 @@ contract Marketplace is Ownable { } function endLease(EndLeaseArgs memory args) public { - RentalInfo storage rental = rentals[args.rentalProduct][args.tokenId][ + RentalInfo storage rental = rentals[args.accessToken][args.tokenId][ args.tenant ]; // The lease can be ended only if the term is over or the rent is insufficient @@ -299,24 +216,23 @@ contract Marketplace is Ownable { "cannot end lease" ); - // Remove the tenant from the rental product - RentalProduct(args.rentalProduct).revokeUser(args.tokenId); + // Burn tenant's access token + AccessToken(args.accessToken).burn(args.tokenId); rental.status = RentalStatus.EndedOrNotExist; } function withdraw(WithdrawArgs memory args) public { - ListingInfo storage listing = listings[args.rentalProduct][ + ListingInfo storage listing = listings[args.accessToken][ args.tokenId ]; require(listing.owner == msg.sender, "not listing owner"); require( - RentalProduct(args.rentalProduct).isUser(args.tokenId, address(0)), - "rental product has user" + AccessToken(args.accessToken).ownerOf(args.tokenId) == address(0), + "access token has tenant" ); listing.status = ListingStatus.WithdrawnOrNotExist; - // fallback call: Transfer the nft back to the owner - IProduct(args.rentalProduct).safeTransferFrom( + (AccessToken(args.accessToken).PRODUCT()).safeTransferFrom( address(this), listing.owner, args.tokenId diff --git a/src/RentalProduct.sol b/src/RentalProduct.sol deleted file mode 100644 index 06b4faf..0000000 --- a/src/RentalProduct.sol +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {IProduct} from "./interfaces/IProduct.sol"; - -contract RentalProduct { - IProduct public immutable PRODUCT; - - mapping(uint256 => address) public getUserByTokenId; - - constructor(IProduct product) { - PRODUCT = product; - } - - modifier onlyTokenOwner(uint256 tokenId) { - require( - msg.sender == PRODUCT.ownerOf(tokenId), - "not token owner" - ); - _; - } - - function addUser( - uint256 tokenId, - address user - ) external onlyTokenOwner(tokenId) { - getUserByTokenId[tokenId] = user; - } - - function revokeUser(uint256 tokenId) external onlyTokenOwner(tokenId) { - getUserByTokenId[tokenId] = address(0); - } - - function isUser(uint256 tokenId, address user) public view returns (bool) { - return getUserByTokenId[tokenId] == user; - } - - function _fallback(address logic) internal { - assembly { - let ptr := mload(0x40) - - // (1) copy incoming call data - calldatacopy(ptr, 0, calldatasize()) - - // (2) forward call to logic contract - let result := call( - gas(), - logic, - callvalue(), - ptr, - calldatasize(), - 0, - 0 - ) - let size := returndatasize() - - // (3) retrieve return data - returndatacopy(ptr, 0, size) - - // (4) forward return data back to caller - switch result - case 0 { - revert(ptr, size) - } - default { - return(ptr, size) - } - } - } - - fallback() external payable { - _fallback(address(PRODUCT)); - } - - receive() external payable {} -} diff --git a/src/RentalProductFactory.sol b/src/RentalProductFactory.sol deleted file mode 100644 index 029ae90..0000000 --- a/src/RentalProductFactory.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {IProduct} from "./interfaces/IProduct.sol"; -import {RentalProduct} from "./RentalProduct.sol"; - -contract RentalProductFactory { - mapping (address => address) public getRentalProduct; - - function createRentalProduct( - address productAddress - ) external returns (address) - { - require( - address(getRentalProduct[productAddress]) == address(0), - "existing rental product" - ); - - RentalProduct rentalProduct = new RentalProduct(IProduct(productAddress)); - getRentalProduct[productAddress] = address(rentalProduct); - return address(rentalProduct); - } -} \ No newline at end of file diff --git a/src/interfaces/IMarketplace.sol b/src/interfaces/IMarketplace.sol new file mode 100644 index 0000000..9d8091d --- /dev/null +++ b/src/interfaces/IMarketplace.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IMarketplace { + // Statuses of a listing. WithdrawnOrNotExist, which is 0, is effectively the same as never listed before. + enum ListingStatus { + WithdrawnOrNotExist, + Listing, + Delisted + } + + // Statuses of a rental. EndedOrNotExist, which is 0, is effectively the same as never exist before. + enum RentalStatus { + EndedOrNotExist, + Renting + } + + struct ListingInfo { + address owner; + uint256 minRentalDays; + uint256 maxRentalDays; + address rentCurrency; + uint256 dailyRent; + address payable rentRecipient; + ListingStatus status; + } + + struct RentalInfo { + uint256 startTime; + uint256 endTime; + uint256 rentalDays; + address rentCurrency; + uint256 dailyRent; + uint256 totalPaidRent; + RentalStatus status; + } + + struct ListArgs { + address product; + uint256 tokenId; + uint256 minRentalDays; + uint256 maxRentalDays; + address rentCurrency; + uint256 dailyRent; + address rentRecipient; + } + + struct DelistArgs { + address payable accessToken; + uint256 tokenId; + } + + struct RelistArgs { + address accessToken; + uint256 tokenId; + uint256 minRentalDays; + uint256 maxRentalDays; + address rentCurrency; + uint256 dailyRent; + address payable rentRecipient; + } + + struct RentArgs { + address accessToken; + uint256 tokenId; + address tenant; + uint256 rentalDays; + uint256 prepaidRent; + } + + struct PayRentArgs { + address accessToken; + uint256 tokenId; + address tenant; + uint256 rent; + } + + struct EndLeaseArgs { + address accessToken; + uint256 tokenId; + address tenant; + } + + struct WithdrawArgs { + address accessToken; + uint256 tokenId; + } +} \ No newline at end of file