From e93da82cf5c3d73d253128437eb9d0bc1eee20fe Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Wed, 17 Apr 2024 15:01:14 -0300 Subject: [PATCH 1/6] ON-813: ERC721 Marketplace - Accept Rental Offer --- contracts/NftRentalMarketplace.sol | 59 ++++- .../libraries/LibOriumNftMarketplace.sol | 201 +++++++++++++++ test/NftRentalMarketplace.test.ts | 242 +++++++++++++++++- utils/types.ts | 1 - 4 files changed, 495 insertions(+), 8 deletions(-) create mode 100644 contracts/libraries/LibOriumNftMarketplace.sol diff --git a/contracts/NftRentalMarketplace.sol b/contracts/NftRentalMarketplace.sol index 39deca0..4222298 100644 --- a/contracts/NftRentalMarketplace.sol +++ b/contracts/NftRentalMarketplace.sol @@ -3,12 +3,13 @@ pragma solidity 0.8.9; import { IERC721 } from '@openzeppelin/contracts/token/ERC721/IERC721.sol'; -import { IERC7432VaultExtension } from './interfaces/IERC7432VaultExtension.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import { IOriumMarketplaceRoyalties } from './interfaces/IOriumMarketplaceRoyalties.sol'; import { OwnableUpgradeable } from '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol'; import { Initializable } from '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol'; import { PausableUpgradeable } from '@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol'; import { LibNftRentalMarketplace, RentalOffer } from './libraries/LibNftRentalMarketplace.sol'; +import { LibOriumNftMarketplace, RentalOffer, Rental } from './libraries/LibOriumNftMarketplace.sol'; /** * @title Orium NFT Marketplace - Marketplace for renting NFTs @@ -30,6 +31,9 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr /// @dev role => tokenAddress => tokenId => deadline mapping(bytes32 => mapping(address => mapping(uint256 => uint64))) public roleDeadline; + /// @dev hashedOffer => Rental + mapping(bytes32 => Rental) public rentals; + /** ######### Events ########### **/ /** @@ -58,6 +62,14 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr bytes[] rolesData ); + /** + * @param lender The address of the lender + * @param nonce The nonce of the rental offer + * @param borrower The address of the borrower + * @param expirationDate The expiration date of the rental + */ + event RentalStarted(address indexed lender, uint256 indexed nonce, address indexed borrower, uint64 expirationDate); + /** ######### Initializer ########### **/ /** * @notice Initializes the contract. @@ -116,6 +128,51 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr ); } + /** + * @notice Accepts a rental offer. + * @dev The borrower can be address(0) to allow anyone to rent the NFT. + * @param _offer The rental offer struct. It should be the same as the one used to create the offer. + * @param _duration The duration of the rental. + */ + function acceptRentalOffer(RentalOffer calldata _offer, uint64 _duration) external whenNotPaused { + bytes32 _offerHash = LibOriumNftMarketplace.hashRentalOffer(_offer); + uint64 _expirationDate = uint64(block.timestamp + _duration); + + LibOriumNftMarketplace.validateAcceptRentalOfferParams( + _offer.borrower, + _offer.minDuration, + isCreated[_offerHash], + rentals[_offerHash].expirationDate, + _duration, + nonceDeadline[_offer.lender][_offer.nonce], + _expirationDate + ); + + LibOriumNftMarketplace.transferFees( + _offer.feeTokenAddress, + owner(), + _offer.lender, + oriumMarketplaceRoyalties, + _offer.tokenAddress, + _offer.feeAmountPerSecond, + _duration + ); + + LibOriumNftMarketplace.batchGrantRole( + oriumMarketplaceRoyalties, + _offer.tokenAddress, + _offer.tokenId, + _offer.borrower, + _expirationDate, + _offer.roles, + _offer.rolesData + ); + + rentals[_offerHash] = Rental({ borrower: msg.sender, expirationDate: _expirationDate }); + + emit RentalStarted(_offer.lender, _offer.nonce, msg.sender, _expirationDate); + } + /** ============================ Core Functions ================================== **/ /** ######### Setters ########### **/ diff --git a/contracts/libraries/LibOriumNftMarketplace.sol b/contracts/libraries/LibOriumNftMarketplace.sol new file mode 100644 index 0000000..36e81cd --- /dev/null +++ b/contracts/libraries/LibOriumNftMarketplace.sol @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity 0.8.9; + +import { IERC7589 } from '../interfaces/IERC7589.sol'; +import { IOriumMarketplaceRoyalties } from '../interfaces/IOriumMarketplaceRoyalties.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import { IERC7432 } from '../interfaces/IERC7432.sol'; +/// @dev Rental offer info. +struct RentalOffer { + address lender; + address borrower; + address tokenAddress; + uint256 tokenId; + address feeTokenAddress; + uint256 feeAmountPerSecond; + uint256 nonce; + uint64 deadline; + uint64 minDuration; + bytes32[] roles; + bytes[] rolesData; +} + +/// @dev Rental info. +struct Rental { + address borrower; + uint64 expirationDate; +} + +library LibOriumNftMarketplace { + /// @dev 100 ether is 100% + uint256 public constant MAX_PERCENTAGE = 100 ether; + + /** + * @notice Gets the rental offer hash. + * @dev This function is used to hash the rental offer struct with retrocompatibility. + * is only used for reading the hash from the storage. + * @param _offer The rental offer struct to be hashed. + */ + function hashRentalOffer(RentalOffer memory _offer) external pure returns (bytes32) { + return keccak256(abi.encode(_offer)); + } + + /** + * @dev All values needs to be in wei. + * @param _amount The amount to calculate the percentage from. + * @param _percentage The percentage to calculate. + */ + function getAmountFromPercentage(uint256 _amount, uint256 _percentage) public pure returns (uint256) { + return (_amount * _percentage) / MAX_PERCENTAGE; + } + + /** + * @notice Validates the rental offer. + * @param _offer The rental offer struct to be validated. + */ + function validateCreateRentalOfferParams( + address _oriumMarketplaceRoyalties, + RentalOffer memory _offer, + uint64 _nonceDeadline + ) external view { + require( + IOriumMarketplaceRoyalties(_oriumMarketplaceRoyalties).isTrustedFeeTokenAddressForToken( + _offer.tokenAddress, + _offer.feeTokenAddress + ), + 'OriumNftMarketplace: tokenAddress is not trusted' + ); + require( + _offer.deadline <= block.timestamp + IOriumMarketplaceRoyalties(_oriumMarketplaceRoyalties).maxDuration() && + _offer.deadline > block.timestamp, + 'OriumNftMarketplace: Invalid deadline' + ); + require(_offer.nonce != 0, 'OriumNftMarketplace: Nonce cannot be 0'); + require(msg.sender == _offer.lender, 'OriumNftMarketplace: Sender and Lender mismatch'); + require(_offer.roles.length > 0, 'OriumNftMarketplace: roles should not be empty'); + require( + _offer.roles.length == _offer.rolesData.length, + 'OriumNftMarketplace: roles and rolesData should have the same length' + ); + require( + _offer.borrower != address(0) || _offer.feeAmountPerSecond > 0, + 'OriumNftMarketplace: feeAmountPerSecond should be greater than 0' + ); + require(_offer.minDuration <= _offer.deadline - block.timestamp, 'OriumNftMarketplace: minDuration is invalid'); + require(_nonceDeadline == 0, 'OriumNftMarketplace: nonce already used'); + } + + /** + * @notice Transfers the fees. + * @dev The fee token address should be approved before calling this function. + * @param _feeTokenAddress The fee token address. + * @param _marketplaceTreasuryAddress The marketplace treasury address. + * @param _lenderAddress The lender address. + * @param _royaltiesAddress The Orium marketplace royalties contract address. + * @param _tokenAddress The token address. + * @param _feeAmountPerSecond The fee amount per second. + * @param _duration The duration of the rental. + */ + function transferFees( + address _feeTokenAddress, + address _marketplaceTreasuryAddress, + address _lenderAddress, + address _royaltiesAddress, + address _tokenAddress, + uint256 _feeAmountPerSecond, + uint64 _duration + ) external { + uint256 _totalAmount = _feeAmountPerSecond * _duration; + if (_totalAmount == 0) return; + + IOriumMarketplaceRoyalties _royalties = IOriumMarketplaceRoyalties(_royaltiesAddress); + uint256 _marketplaceFeePercentageInWei = _royalties.marketplaceFeeOf(_tokenAddress); + IOriumMarketplaceRoyalties.RoyaltyInfo memory _royaltyInfo = _royalties.royaltyInfoOf(_tokenAddress); + + uint256 _marketplaceAmount = getAmountFromPercentage(_totalAmount, _marketplaceFeePercentageInWei); + uint256 _royaltyAmount = getAmountFromPercentage(_totalAmount, _royaltyInfo.royaltyPercentageInWei); + uint256 _lenderAmount = _totalAmount - _royaltyAmount - _marketplaceAmount; + + _transferAmount(_feeTokenAddress, msg.sender, _marketplaceTreasuryAddress, _marketplaceAmount); + _transferAmount(_feeTokenAddress, msg.sender, _royaltyInfo.treasury, _royaltyAmount); + _transferAmount(_feeTokenAddress, msg.sender, _lenderAddress, _lenderAmount); + } + + /** + * @notice Transfers an amount to a receipient. + * @dev This function is used to make an ERC20 transfer. + * @param _tokenAddress The token address. + * @param _from The sender address. + * @param _to The recipient address. + * @param _amount The amount to transfer. + */ + function _transferAmount(address _tokenAddress, address _from, address _to, uint256 _amount) internal { + if (_amount == 0) return; + require(IERC20(_tokenAddress).transferFrom(_from, _to, _amount), 'OriumNftMarketplace: Transfer failed'); + } + + function validateAcceptRentalOfferParams( + address _borrower, + uint64 _minDuration, + bool _isCreated, + uint64 _previousRentalExpirationDate, + uint64 _duration, + uint256 _nonceDeadline, + uint64 _expirationDate + ) external view { + require(_isCreated, 'OriumNftMarketplace: Offer not created'); + require( + _previousRentalExpirationDate <= block.timestamp, + 'OriumNftMarketplace: This offer has an ongoing rental' + ); + require(_duration >= _minDuration, 'OriumNftMarketplace: Duration is less than the offer minimum duration'); + require( + _nonceDeadline > _expirationDate, + 'OriumNftMarketplace: expiration date is greater than offer deadline' + ); + require( + address(0) == _borrower || msg.sender == _borrower, + 'OriumNftMarketplace: Sender is not allowed to rent this NFT' + ); + } + + /** + * @notice Batch grants roles. + * @dev This function is used to batch grant roles. + * @param _oriumMarketplaceRoyalties The Orium marketplace royalties contract address. + * @param _tokenAddress The token address. + * @param _tokenId The token id. + * @param _recipient The recipient address. + * @param _expirationDate The expiration date. + * @param _roleIds The role ids. + * @param _data The data. + */ + function batchGrantRole( + address _oriumMarketplaceRoyalties, + address _tokenAddress, + uint256 _tokenId, + address _recipient, + uint64 _expirationDate, + bytes32[] calldata _roleIds, + bytes[] calldata _data + ) external { + address _rolesRegsitry = IOriumMarketplaceRoyalties(_oriumMarketplaceRoyalties).nftRolesRegistryOf( + _tokenAddress + ); + + for (uint256 i = 0; i < _roleIds.length; i++) { + IERC7432(_rolesRegsitry).grantRole( + IERC7432.Role({ + roleId: _roleIds[i], + tokenAddress: _tokenAddress, + tokenId: _tokenId, + recipient: _recipient, + expirationDate: _expirationDate, + revocable: false, + data: _data[i] + }) + ); + } + } +} diff --git a/test/NftRentalMarketplace.test.ts b/test/NftRentalMarketplace.test.ts index e9dab70..237aa61 100644 --- a/test/NftRentalMarketplace.test.ts +++ b/test/NftRentalMarketplace.test.ts @@ -1,9 +1,10 @@ +/* eslint-disable no-unexpected-multiline */ import { ethers } from 'hardhat' import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers' import { expect } from 'chai' import { toWei } from '../utils/bignumber' -import { RentalOffer } from '../utils/types' -import { AddressZero, EMPTY_BYTES, ONE_DAY, THREE_MONTHS } from '../utils/constants' +import { RentalOffer, RoyaltyInfo } from '../utils/types' +import { AddressZero, EMPTY_BYTES, ONE_DAY, ONE_HOUR, THREE_MONTHS } from '../utils/constants' import { randomBytes } from 'crypto' import { USER_ROLE } from '../utils/roles' import { IERC7432, MockERC20, MockERC721, OriumMarketplaceRoyalties, NftRentalMarketplace } from '../typechain-types' @@ -22,16 +23,19 @@ describe('NftRentalMarketplace', () => { let operator: Awaited> let notOperator: Awaited> let creator: Awaited> + let creatorTreasury: Awaited> let lender: Awaited> + let borrower: Awaited> // Values to be used across tests const maxDeadline = THREE_MONTHS const tokenId = 1 + const duration = ONE_HOUR before(async function () { // we are disabling this rule so ; may not be added automatically by prettier at the beginning of the line // prettier-ignore - [deployer, operator, notOperator, creator, lender] = await ethers.getSigners() + [deployer, operator, notOperator, creator, creatorTreasury, lender, borrower] = await ethers.getSigners() }) beforeEach(async () => { @@ -59,7 +63,26 @@ describe('NftRentalMarketplace', () => { let rentalOffer: RentalOffer beforeEach(async () => { - const blockTimestamp = Number((await ethers.provider.getBlock('latest'))?.timestamp) + await marketplaceRoyalties + .connect(operator) + .setRoyaltyInfo(creator.address, await mockERC721.getAddress(), 0, AddressZero) + + const royaltyInfo: RoyaltyInfo = { + creator: creator.address, + royaltyPercentageInWei: toWei('10'), + treasury: creatorTreasury.address, + } + + await marketplaceRoyalties + .connect(creator) + .setRoyaltyInfo( + creator.address, + await mockERC721.getAddress(), + royaltyInfo.royaltyPercentageInWei, + royaltyInfo.treasury, + ) + + const blockTimestamp = Number(await time.latest()) rentalOffer = { nonce: `0x${randomBytes(32).toString('hex')}`, @@ -117,7 +140,7 @@ describe('NftRentalMarketplace', () => { await marketplace.connect(lender).createRentalOffer(rentalOffer) await time.increase(ONE_DAY) rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` - rentalOffer.deadline = Number((await ethers.provider.getBlock('latest'))?.timestamp) + ONE_DAY + rentalOffer.deadline = Number(await time.latest()) + ONE_DAY await expect(marketplace.connect(lender).createRentalOffer(rentalOffer)) .to.emit(marketplace, 'RentalOfferCreated') .withArgs( @@ -193,7 +216,7 @@ describe('NftRentalMarketplace', () => { ) }) it("Should NOT create a rental offer if deadline is less than block's timestamp", async () => { - rentalOffer.deadline = Number((await ethers.provider.getBlock('latest'))?.timestamp) - 1 + rentalOffer.deadline = Number(await time.latest()) - 1 await expect(marketplace.connect(lender).createRentalOffer(rentalOffer)).to.be.revertedWith( 'NftRentalMarketplace: Invalid deadline', ) @@ -245,6 +268,213 @@ describe('NftRentalMarketplace', () => { }) }) }) + + describe('When Rental Offer is created', async () => { + let totalFeeAmount: bigint + beforeEach(async () => { + totalFeeAmount = rentalOffer.feeAmountPerSecond * BigInt(duration) + await marketplace.connect(lender).createRentalOffer(rentalOffer) + await mockERC20.mint(borrower.address, totalFeeAmount.toString()) + await mockERC20.connect(borrower).approve(await marketplace.getAddress(), totalFeeAmount.toString()) + await mockERC721.connect(lender).setApprovalForAll(await rolesRegistry.getAddress(), true) + }) + describe('Accept Rental Offer', async () => { + it('Should accept a public rental offer', async () => { + const blockTimestamp = await time.latest() + const expirationDate = Number(blockTimestamp) + duration + 1 + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)) + .to.emit(marketplace, 'RentalStarted') + .withArgs(rentalOffer.lender, rentalOffer.nonce, borrower.address, expirationDate) + }) + it('Should accept a private rental offer', async () => { + await time.increase(ONE_DAY) + rentalOffer.borrower = borrower.address + rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` + rentalOffer.deadline = Number(await time.latest()) + ONE_DAY + rentalOffer.feeAmountPerSecond = toWei('0') + await marketplace.connect(lender).createRentalOffer(rentalOffer) + + const blockTimestamp = await time.latest() + const expirationDate = Number(blockTimestamp) + duration + 1 + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)) + .to.emit(marketplace, 'RentalStarted') + .withArgs(rentalOffer.lender, rentalOffer.nonce, borrower.address, expirationDate) + }) + it('Should accept a rental offer if token has a different registry', async () => { + await marketplaceRoyalties + .connect(operator) + .setRolesRegistry(await mockERC721.getAddress(), await rolesRegistry.getAddress()) + const blockTimestamp = await time.latest() + const expirationDate = Number(blockTimestamp) + duration + 1 + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)) + .to.emit(marketplace, 'RentalStarted') + .withArgs(rentalOffer.lender, rentalOffer.nonce, borrower.address, expirationDate) + }) + it('Should accept a rental offer more than once', async () => { + const rentalExpirationDate1 = Number(await time.latest()) + duration + 1 + + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)) + .to.emit(marketplace, 'RentalStarted') + .withArgs(rentalOffer.lender, rentalOffer.nonce, borrower.address, rentalExpirationDate1) + + await ethers.provider.send('evm_increaseTime', [duration + 1]) + await mockERC20.mint(borrower.address, totalFeeAmount.toString()) + await mockERC20.connect(borrower).approve(await marketplace.getAddress(), totalFeeAmount.toString()) + + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)) + .to.emit(marketplace, 'RentalStarted') + .withArgs(rentalOffer.lender, rentalOffer.nonce, borrower.address, rentalExpirationDate1 + duration + 3) + }) + it('Should accept a rental offer by anyone if borrower is the zero address', async () => { + await time.increase(ONE_DAY) + rentalOffer.borrower = ethers.ZeroAddress + rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` + rentalOffer.deadline = Number(await time.latest()) + ONE_DAY + await marketplace.connect(lender).createRentalOffer(rentalOffer) + + const blockTimestamp = Number(await time.latest()) + await mockERC20.mint(notOperator.address, totalFeeAmount.toString()) + await mockERC20.connect(notOperator).approve(await marketplace.getAddress(), totalFeeAmount.toString()) + await expect(marketplace.connect(notOperator).acceptRentalOffer(rentalOffer, duration)) + .to.emit(marketplace, 'RentalStarted') + .withArgs(rentalOffer.lender, rentalOffer.nonce, notOperator.address, blockTimestamp + duration + 3) + }) + it('Should accept a rental offer if duration is greater or equal minDuration', async () => { + await time.increase(ONE_DAY) + rentalOffer.minDuration = duration / 2 + rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` + rentalOffer.deadline = Number(await time.latest()) + ONE_DAY + await marketplace.connect(lender).createRentalOffer(rentalOffer) + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.emit( + marketplace, + 'RentalStarted', + ) + }) + it('Should accept rental offer if marketplace fee is zero', async () => { + await marketplaceRoyalties + .connect(operator) + .setMarketplaceFeeForCollection(await mockERC721.getAddress(), 0, true) + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.emit( + marketplace, + 'RentalStarted', + ) + }) + it('Should NOT accept a rental offer if duration is less than minDuration', async () => { + await time.increase(ONE_DAY) + rentalOffer.minDuration = duration + rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` + rentalOffer.deadline = Number(await time.latest()) + ONE_DAY + await marketplace.connect(lender).createRentalOffer(rentalOffer) + await expect( + marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration / 2), + ).to.be.revertedWith('OriumNftMarketplace: Duration is less than the offer minimum duration') + }) + it('Should NOT accept a rental offer if contract is paused', async () => { + await marketplace.connect(operator).pause() + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( + 'Pausable: paused', + ) + }) + it('Should NOT accept a rental offer if caller is not the borrower', async () => { + await time.increase(ONE_DAY) + rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` + rentalOffer.borrower = borrower.address + rentalOffer.deadline = Number(await time.latest()) + ONE_DAY + await marketplace.connect(lender).createRentalOffer(rentalOffer) + await mockERC20.mint(notOperator.address, totalFeeAmount.toString()) + await expect( + marketplace.connect(notOperator).acceptRentalOffer(rentalOffer, duration), + ).to.be.revertedWith('OriumNftMarketplace: Sender is not allowed to rent this NFT') + }) + it('Should NOT accept a rental offer if offer is expired', async () => { + // move foward in time to expire the offer + const blockTimestamp = await time.latest() + const timeToMove = rentalOffer.deadline - Number(blockTimestamp) + 1 + await ethers.provider.send('evm_increaseTime', [timeToMove]) + + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( + 'OriumNftMarketplace: expiration date is greater than offer deadline', + ) + }) + it('Should NOT accept a rental offer if offer is not created', async () => { + rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( + 'OriumNftMarketplace: Offer not created', + ) + }) + it('Should NOT accept a rental offer if expiration date is higher than offer deadline', async () => { + const maxDuration = rentalOffer.deadline - Number(await time.latest()) + 1 + await expect( + marketplace.connect(borrower).acceptRentalOffer(rentalOffer, maxDuration), + ).to.be.revertedWith('OriumNftMarketplace: expiration date is greater than offer deadline') + }) + describe('Fees', async function () { + const feeAmountPerSecond = toWei('1') + const feeAmount = feeAmountPerSecond * BigInt(duration) + + beforeEach(async () => { + await time.increase(ONE_DAY) + rentalOffer.feeAmountPerSecond = feeAmountPerSecond + rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` + rentalOffer.deadline = Number(await time.latest()) + ONE_DAY + await marketplace.connect(lender).createRentalOffer(rentalOffer) + await mockERC20.mint(borrower.address, feeAmount * BigInt(2)) + await mockERC20.connect(borrower).approve(await marketplace.getAddress(), feeAmount * BigInt(2)) + }) + + it('Should accept a rental offer with fee', async () => { + const blockTimestamp = await time.latest() + const expirationDate = Number(blockTimestamp) + duration + 1 + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)) + .to.emit(marketplace, 'RentalStarted') + .withArgs(rentalOffer.lender, rentalOffer.nonce, borrower.address, expirationDate) + .to.emit(mockERC20, 'Transfer') + }) + it('Should accept a rental offer if marketplace fee is 0', async () => { + await marketplaceRoyalties + .connect(operator) + .setMarketplaceFeeForCollection(await mockERC721.getAddress(), 0, true) + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.emit( + marketplace, + 'RentalStarted', + ) + }) + it('Should accept a rental offer if royalty fee is 0', async () => { + await marketplaceRoyalties + .connect(creator) + .setRoyaltyInfo(creator.address, await mockERC721.getAddress(), '0', creatorTreasury.address) + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.emit( + marketplace, + 'RentalStarted', + ) + }) + it('Should NOT accept a rental offer if marketplace fee transfer fails', async () => { + await mockERC20.transferReverts(true, 0) + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( + 'OriumNftMarketplace: Transfer failed', + ) + }) + it('Should NOT accept a rental offer if royalty fee transfer fails', async () => { + await mockERC20.transferReverts(true, 1) + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( + 'OriumNftMarketplace: Transfer failed', + ) + }) + it('Should NOT accept a rental offer if lender fee transfer fails', async () => { + await mockERC20.transferReverts(true, 2) + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( + 'OriumNftMarketplace: Transfer failed', + ) + }) + it('Should NOT accept a rental offer twice', async () => { + await marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration) + await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( + 'OriumNftMarketplace: This offer has an ongoing rental', + ) + }) + }) + }) + }) }) }) describe('Core Functions', async () => { diff --git a/utils/types.ts b/utils/types.ts index aea4ff9..2e436b9 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -34,7 +34,6 @@ export interface DirectRental { } export interface SftRentalOffer extends RentalOffer { - minDuration: number tokenAmount: bigint commitmentId: bigint } From a6861bdbb7a5ffe5da1822deec54cdf5cca9bac2 Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Fri, 19 Apr 2024 15:41:41 -0300 Subject: [PATCH 2/6] ON-813: fix rebase --- contracts/NftRentalMarketplace.sol | 11 +- .../libraries/LibNftRentalMarketplace.sol | 134 ++++++++++++ .../libraries/LibOriumNftMarketplace.sol | 201 ------------------ 3 files changed, 139 insertions(+), 207 deletions(-) delete mode 100644 contracts/libraries/LibOriumNftMarketplace.sol diff --git a/contracts/NftRentalMarketplace.sol b/contracts/NftRentalMarketplace.sol index 4222298..504bd73 100644 --- a/contracts/NftRentalMarketplace.sol +++ b/contracts/NftRentalMarketplace.sol @@ -8,8 +8,7 @@ import { IOriumMarketplaceRoyalties } from './interfaces/IOriumMarketplaceRoyalt import { OwnableUpgradeable } from '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol'; import { Initializable } from '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol'; import { PausableUpgradeable } from '@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol'; -import { LibNftRentalMarketplace, RentalOffer } from './libraries/LibNftRentalMarketplace.sol'; -import { LibOriumNftMarketplace, RentalOffer, Rental } from './libraries/LibOriumNftMarketplace.sol'; +import { LibNftRentalMarketplace, RentalOffer, Rental } from './libraries/LibNftRentalMarketplace.sol'; /** * @title Orium NFT Marketplace - Marketplace for renting NFTs @@ -135,10 +134,10 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr * @param _duration The duration of the rental. */ function acceptRentalOffer(RentalOffer calldata _offer, uint64 _duration) external whenNotPaused { - bytes32 _offerHash = LibOriumNftMarketplace.hashRentalOffer(_offer); + bytes32 _offerHash = LibNftRentalMarketplace.hashRentalOffer(_offer); uint64 _expirationDate = uint64(block.timestamp + _duration); - LibOriumNftMarketplace.validateAcceptRentalOfferParams( + LibNftRentalMarketplace.validateAcceptRentalOfferParams( _offer.borrower, _offer.minDuration, isCreated[_offerHash], @@ -148,7 +147,7 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr _expirationDate ); - LibOriumNftMarketplace.transferFees( + LibNftRentalMarketplace.transferFees( _offer.feeTokenAddress, owner(), _offer.lender, @@ -158,7 +157,7 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr _duration ); - LibOriumNftMarketplace.batchGrantRole( + LibNftRentalMarketplace.grantRoles( oriumMarketplaceRoyalties, _offer.tokenAddress, _offer.tokenId, diff --git a/contracts/libraries/LibNftRentalMarketplace.sol b/contracts/libraries/LibNftRentalMarketplace.sol index 578b9e4..94931ed 100644 --- a/contracts/libraries/LibNftRentalMarketplace.sol +++ b/contracts/libraries/LibNftRentalMarketplace.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.9; import { IERC721 } from '@openzeppelin/contracts/token/ERC721/IERC721.sol'; +import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import { IERC7432 } from '../interfaces/IERC7432.sol'; import { IERC7432VaultExtension } from '../interfaces/IERC7432VaultExtension.sol'; import { IOriumMarketplaceRoyalties } from '../interfaces/IOriumMarketplaceRoyalties.sol'; @@ -21,7 +23,16 @@ struct RentalOffer { bytes[] rolesData; } +/// @dev Rental info. +struct Rental { + address borrower; + uint64 expirationDate; +} + library LibNftRentalMarketplace { + /// @dev 100 ether is 100% + uint256 public constant MAX_PERCENTAGE = 100 ether; + /** * @notice Gets the rental offer hash. * @dev This function is used to hash the rental offer struct @@ -79,4 +90,127 @@ library LibNftRentalMarketplace { ); require(_nonceDeadline == 0, 'NftRentalMarketplace: nonce already used'); } + + /** + * @dev All values needs to be in wei. + * @param _amount The amount to calculate the percentage from. + * @param _percentage The percentage to calculate. + */ + function getAmountFromPercentage(uint256 _amount, uint256 _percentage) public pure returns (uint256) { + return (_amount * _percentage) / MAX_PERCENTAGE; + } + + /** + * @notice Transfers the fees. + * @dev The fee token address should be approved before calling this function. + * @param _feeTokenAddress The fee token address. + * @param _marketplaceTreasuryAddress The marketplace treasury address. + * @param _lenderAddress The lender address. + * @param _royaltiesAddress The Orium marketplace royalties contract address. + * @param _tokenAddress The token address. + * @param _feeAmountPerSecond The fee amount per second. + * @param _duration The duration of the rental. + */ + function transferFees( + address _feeTokenAddress, + address _marketplaceTreasuryAddress, + address _lenderAddress, + address _royaltiesAddress, + address _tokenAddress, + uint256 _feeAmountPerSecond, + uint64 _duration + ) external { + uint256 _totalAmount = _feeAmountPerSecond * _duration; + if (_totalAmount == 0) return; + + IOriumMarketplaceRoyalties _royalties = IOriumMarketplaceRoyalties(_royaltiesAddress); + uint256 _marketplaceFeePercentageInWei = _royalties.marketplaceFeeOf(_tokenAddress); + IOriumMarketplaceRoyalties.RoyaltyInfo memory _royaltyInfo = _royalties.royaltyInfoOf(_tokenAddress); + + uint256 _marketplaceAmount = getAmountFromPercentage(_totalAmount, _marketplaceFeePercentageInWei); + uint256 _royaltyAmount = getAmountFromPercentage(_totalAmount, _royaltyInfo.royaltyPercentageInWei); + uint256 _lenderAmount = _totalAmount - _royaltyAmount - _marketplaceAmount; + + _transferAmount(_feeTokenAddress, msg.sender, _marketplaceTreasuryAddress, _marketplaceAmount); + _transferAmount(_feeTokenAddress, msg.sender, _royaltyInfo.treasury, _royaltyAmount); + _transferAmount(_feeTokenAddress, msg.sender, _lenderAddress, _lenderAmount); + } + + /** + * @notice Transfers an amount to a receipient. + * @dev This function is used to make an ERC20 transfer. + * @param _tokenAddress The token address. + * @param _from The sender address. + * @param _to The recipient address. + * @param _amount The amount to transfer. + */ + function _transferAmount(address _tokenAddress, address _from, address _to, uint256 _amount) internal { + if (_amount == 0) return; + require(IERC20(_tokenAddress).transferFrom(_from, _to, _amount), 'OriumNftMarketplace: Transfer failed'); + } + + function validateAcceptRentalOfferParams( + address _borrower, + uint64 _minDuration, + bool _isCreated, + uint64 _previousRentalExpirationDate, + uint64 _duration, + uint256 _nonceDeadline, + uint64 _expirationDate + ) external view { + require(_isCreated, 'OriumNftMarketplace: Offer not created'); + require( + _previousRentalExpirationDate <= block.timestamp, + 'OriumNftMarketplace: This offer has an ongoing rental' + ); + require(_duration >= _minDuration, 'OriumNftMarketplace: Duration is less than the offer minimum duration'); + require( + _nonceDeadline > _expirationDate, + 'OriumNftMarketplace: expiration date is greater than offer deadline' + ); + require( + address(0) == _borrower || msg.sender == _borrower, + 'OriumNftMarketplace: Sender is not allowed to rent this NFT' + ); + } + + /** + * @notice grants roles for the same NFT. + * @dev This function is used to batch grant roles for the same NFT. + * @param _oriumMarketplaceRoyalties The Orium marketplace royalties contract address. + * @param _tokenAddress The token address. + * @param _tokenId The token id. + * @param _recipient The recipient address. + * @param _expirationDate The expiration date. + * @param _roleIds The role ids. + * @param _data The data. + */ + function grantRoles( + address _oriumMarketplaceRoyalties, + address _tokenAddress, + uint256 _tokenId, + address _recipient, + uint64 _expirationDate, + bytes32[] calldata _roleIds, + bytes[] calldata _data + ) external { + address _rolesRegsitry = IOriumMarketplaceRoyalties(_oriumMarketplaceRoyalties).nftRolesRegistryOf( + _tokenAddress + ); + + for (uint256 i = 0; i < _roleIds.length; i++) { + IERC7432(_rolesRegsitry).grantRole( + IERC7432.Role({ + roleId: _roleIds[i], + tokenAddress: _tokenAddress, + tokenId: _tokenId, + recipient: _recipient, + expirationDate: _expirationDate, + revocable: false, + data: _data[i] + }) + ); + } + } + } diff --git a/contracts/libraries/LibOriumNftMarketplace.sol b/contracts/libraries/LibOriumNftMarketplace.sol deleted file mode 100644 index 36e81cd..0000000 --- a/contracts/libraries/LibOriumNftMarketplace.sol +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -pragma solidity 0.8.9; - -import { IERC7589 } from '../interfaces/IERC7589.sol'; -import { IOriumMarketplaceRoyalties } from '../interfaces/IOriumMarketplaceRoyalties.sol'; -import { IERC20 } from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -import { IERC7432 } from '../interfaces/IERC7432.sol'; -/// @dev Rental offer info. -struct RentalOffer { - address lender; - address borrower; - address tokenAddress; - uint256 tokenId; - address feeTokenAddress; - uint256 feeAmountPerSecond; - uint256 nonce; - uint64 deadline; - uint64 minDuration; - bytes32[] roles; - bytes[] rolesData; -} - -/// @dev Rental info. -struct Rental { - address borrower; - uint64 expirationDate; -} - -library LibOriumNftMarketplace { - /// @dev 100 ether is 100% - uint256 public constant MAX_PERCENTAGE = 100 ether; - - /** - * @notice Gets the rental offer hash. - * @dev This function is used to hash the rental offer struct with retrocompatibility. - * is only used for reading the hash from the storage. - * @param _offer The rental offer struct to be hashed. - */ - function hashRentalOffer(RentalOffer memory _offer) external pure returns (bytes32) { - return keccak256(abi.encode(_offer)); - } - - /** - * @dev All values needs to be in wei. - * @param _amount The amount to calculate the percentage from. - * @param _percentage The percentage to calculate. - */ - function getAmountFromPercentage(uint256 _amount, uint256 _percentage) public pure returns (uint256) { - return (_amount * _percentage) / MAX_PERCENTAGE; - } - - /** - * @notice Validates the rental offer. - * @param _offer The rental offer struct to be validated. - */ - function validateCreateRentalOfferParams( - address _oriumMarketplaceRoyalties, - RentalOffer memory _offer, - uint64 _nonceDeadline - ) external view { - require( - IOriumMarketplaceRoyalties(_oriumMarketplaceRoyalties).isTrustedFeeTokenAddressForToken( - _offer.tokenAddress, - _offer.feeTokenAddress - ), - 'OriumNftMarketplace: tokenAddress is not trusted' - ); - require( - _offer.deadline <= block.timestamp + IOriumMarketplaceRoyalties(_oriumMarketplaceRoyalties).maxDuration() && - _offer.deadline > block.timestamp, - 'OriumNftMarketplace: Invalid deadline' - ); - require(_offer.nonce != 0, 'OriumNftMarketplace: Nonce cannot be 0'); - require(msg.sender == _offer.lender, 'OriumNftMarketplace: Sender and Lender mismatch'); - require(_offer.roles.length > 0, 'OriumNftMarketplace: roles should not be empty'); - require( - _offer.roles.length == _offer.rolesData.length, - 'OriumNftMarketplace: roles and rolesData should have the same length' - ); - require( - _offer.borrower != address(0) || _offer.feeAmountPerSecond > 0, - 'OriumNftMarketplace: feeAmountPerSecond should be greater than 0' - ); - require(_offer.minDuration <= _offer.deadline - block.timestamp, 'OriumNftMarketplace: minDuration is invalid'); - require(_nonceDeadline == 0, 'OriumNftMarketplace: nonce already used'); - } - - /** - * @notice Transfers the fees. - * @dev The fee token address should be approved before calling this function. - * @param _feeTokenAddress The fee token address. - * @param _marketplaceTreasuryAddress The marketplace treasury address. - * @param _lenderAddress The lender address. - * @param _royaltiesAddress The Orium marketplace royalties contract address. - * @param _tokenAddress The token address. - * @param _feeAmountPerSecond The fee amount per second. - * @param _duration The duration of the rental. - */ - function transferFees( - address _feeTokenAddress, - address _marketplaceTreasuryAddress, - address _lenderAddress, - address _royaltiesAddress, - address _tokenAddress, - uint256 _feeAmountPerSecond, - uint64 _duration - ) external { - uint256 _totalAmount = _feeAmountPerSecond * _duration; - if (_totalAmount == 0) return; - - IOriumMarketplaceRoyalties _royalties = IOriumMarketplaceRoyalties(_royaltiesAddress); - uint256 _marketplaceFeePercentageInWei = _royalties.marketplaceFeeOf(_tokenAddress); - IOriumMarketplaceRoyalties.RoyaltyInfo memory _royaltyInfo = _royalties.royaltyInfoOf(_tokenAddress); - - uint256 _marketplaceAmount = getAmountFromPercentage(_totalAmount, _marketplaceFeePercentageInWei); - uint256 _royaltyAmount = getAmountFromPercentage(_totalAmount, _royaltyInfo.royaltyPercentageInWei); - uint256 _lenderAmount = _totalAmount - _royaltyAmount - _marketplaceAmount; - - _transferAmount(_feeTokenAddress, msg.sender, _marketplaceTreasuryAddress, _marketplaceAmount); - _transferAmount(_feeTokenAddress, msg.sender, _royaltyInfo.treasury, _royaltyAmount); - _transferAmount(_feeTokenAddress, msg.sender, _lenderAddress, _lenderAmount); - } - - /** - * @notice Transfers an amount to a receipient. - * @dev This function is used to make an ERC20 transfer. - * @param _tokenAddress The token address. - * @param _from The sender address. - * @param _to The recipient address. - * @param _amount The amount to transfer. - */ - function _transferAmount(address _tokenAddress, address _from, address _to, uint256 _amount) internal { - if (_amount == 0) return; - require(IERC20(_tokenAddress).transferFrom(_from, _to, _amount), 'OriumNftMarketplace: Transfer failed'); - } - - function validateAcceptRentalOfferParams( - address _borrower, - uint64 _minDuration, - bool _isCreated, - uint64 _previousRentalExpirationDate, - uint64 _duration, - uint256 _nonceDeadline, - uint64 _expirationDate - ) external view { - require(_isCreated, 'OriumNftMarketplace: Offer not created'); - require( - _previousRentalExpirationDate <= block.timestamp, - 'OriumNftMarketplace: This offer has an ongoing rental' - ); - require(_duration >= _minDuration, 'OriumNftMarketplace: Duration is less than the offer minimum duration'); - require( - _nonceDeadline > _expirationDate, - 'OriumNftMarketplace: expiration date is greater than offer deadline' - ); - require( - address(0) == _borrower || msg.sender == _borrower, - 'OriumNftMarketplace: Sender is not allowed to rent this NFT' - ); - } - - /** - * @notice Batch grants roles. - * @dev This function is used to batch grant roles. - * @param _oriumMarketplaceRoyalties The Orium marketplace royalties contract address. - * @param _tokenAddress The token address. - * @param _tokenId The token id. - * @param _recipient The recipient address. - * @param _expirationDate The expiration date. - * @param _roleIds The role ids. - * @param _data The data. - */ - function batchGrantRole( - address _oriumMarketplaceRoyalties, - address _tokenAddress, - uint256 _tokenId, - address _recipient, - uint64 _expirationDate, - bytes32[] calldata _roleIds, - bytes[] calldata _data - ) external { - address _rolesRegsitry = IOriumMarketplaceRoyalties(_oriumMarketplaceRoyalties).nftRolesRegistryOf( - _tokenAddress - ); - - for (uint256 i = 0; i < _roleIds.length; i++) { - IERC7432(_rolesRegsitry).grantRole( - IERC7432.Role({ - roleId: _roleIds[i], - tokenAddress: _tokenAddress, - tokenId: _tokenId, - recipient: _recipient, - expirationDate: _expirationDate, - revocable: false, - data: _data[i] - }) - ); - } - } -} From ac8f25da61bae986b4b30dc3d3f80e28eb399e9e Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Fri, 19 Apr 2024 16:41:04 -0300 Subject: [PATCH 3/6] ON-813: update accept rental offer function --- contracts/NftRentalMarketplace.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/NftRentalMarketplace.sol b/contracts/NftRentalMarketplace.sol index 504bd73..6563bc5 100644 --- a/contracts/NftRentalMarketplace.sol +++ b/contracts/NftRentalMarketplace.sol @@ -161,7 +161,7 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr oriumMarketplaceRoyalties, _offer.tokenAddress, _offer.tokenId, - _offer.borrower, + msg.sender, _expirationDate, _offer.roles, _offer.rolesData From e984c68f109f85f79596b12281d5afe9ef598453 Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Fri, 19 Apr 2024 16:42:35 -0300 Subject: [PATCH 4/6] ON-813: update tests --- .../libraries/LibNftRentalMarketplace.sol | 12 ++++++------ test/NftRentalMarketplace.test.ts | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contracts/libraries/LibNftRentalMarketplace.sol b/contracts/libraries/LibNftRentalMarketplace.sol index 94931ed..0ed039b 100644 --- a/contracts/libraries/LibNftRentalMarketplace.sol +++ b/contracts/libraries/LibNftRentalMarketplace.sol @@ -146,7 +146,7 @@ library LibNftRentalMarketplace { */ function _transferAmount(address _tokenAddress, address _from, address _to, uint256 _amount) internal { if (_amount == 0) return; - require(IERC20(_tokenAddress).transferFrom(_from, _to, _amount), 'OriumNftMarketplace: Transfer failed'); + require(IERC20(_tokenAddress).transferFrom(_from, _to, _amount), 'NftRentalMarketplace: Transfer failed'); } function validateAcceptRentalOfferParams( @@ -158,19 +158,19 @@ library LibNftRentalMarketplace { uint256 _nonceDeadline, uint64 _expirationDate ) external view { - require(_isCreated, 'OriumNftMarketplace: Offer not created'); + require(_isCreated, 'NftRentalMarketplace: Offer not created'); require( _previousRentalExpirationDate <= block.timestamp, - 'OriumNftMarketplace: This offer has an ongoing rental' + 'NftRentalMarketplace: This offer has an ongoing rental' ); - require(_duration >= _minDuration, 'OriumNftMarketplace: Duration is less than the offer minimum duration'); + require(_duration >= _minDuration, 'NftRentalMarketplace: Duration is less than the offer minimum duration'); require( _nonceDeadline > _expirationDate, - 'OriumNftMarketplace: expiration date is greater than offer deadline' + 'NftRentalMarketplace: expiration date is greater than offer deadline' ); require( address(0) == _borrower || msg.sender == _borrower, - 'OriumNftMarketplace: Sender is not allowed to rent this NFT' + 'NftRentalMarketplace: Sender is not allowed to rent this NFT' ); } diff --git a/test/NftRentalMarketplace.test.ts b/test/NftRentalMarketplace.test.ts index 237aa61..9f12b46 100644 --- a/test/NftRentalMarketplace.test.ts +++ b/test/NftRentalMarketplace.test.ts @@ -367,7 +367,7 @@ describe('NftRentalMarketplace', () => { await marketplace.connect(lender).createRentalOffer(rentalOffer) await expect( marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration / 2), - ).to.be.revertedWith('OriumNftMarketplace: Duration is less than the offer minimum duration') + ).to.be.revertedWith('NftRentalMarketplace: Duration is less than the offer minimum duration') }) it('Should NOT accept a rental offer if contract is paused', async () => { await marketplace.connect(operator).pause() @@ -384,7 +384,7 @@ describe('NftRentalMarketplace', () => { await mockERC20.mint(notOperator.address, totalFeeAmount.toString()) await expect( marketplace.connect(notOperator).acceptRentalOffer(rentalOffer, duration), - ).to.be.revertedWith('OriumNftMarketplace: Sender is not allowed to rent this NFT') + ).to.be.revertedWith('NftRentalMarketplace: Sender is not allowed to rent this NFT') }) it('Should NOT accept a rental offer if offer is expired', async () => { // move foward in time to expire the offer @@ -393,20 +393,20 @@ describe('NftRentalMarketplace', () => { await ethers.provider.send('evm_increaseTime', [timeToMove]) await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( - 'OriumNftMarketplace: expiration date is greater than offer deadline', + 'NftRentalMarketplace: expiration date is greater than offer deadline', ) }) it('Should NOT accept a rental offer if offer is not created', async () => { rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}` await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( - 'OriumNftMarketplace: Offer not created', + 'NftRentalMarketplace: Offer not created', ) }) it('Should NOT accept a rental offer if expiration date is higher than offer deadline', async () => { const maxDuration = rentalOffer.deadline - Number(await time.latest()) + 1 await expect( marketplace.connect(borrower).acceptRentalOffer(rentalOffer, maxDuration), - ).to.be.revertedWith('OriumNftMarketplace: expiration date is greater than offer deadline') + ).to.be.revertedWith('NftRentalMarketplace: expiration date is greater than offer deadline') }) describe('Fees', async function () { const feeAmountPerSecond = toWei('1') @@ -451,25 +451,25 @@ describe('NftRentalMarketplace', () => { it('Should NOT accept a rental offer if marketplace fee transfer fails', async () => { await mockERC20.transferReverts(true, 0) await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( - 'OriumNftMarketplace: Transfer failed', + 'NftRentalMarketplace: Transfer failed', ) }) it('Should NOT accept a rental offer if royalty fee transfer fails', async () => { await mockERC20.transferReverts(true, 1) await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( - 'OriumNftMarketplace: Transfer failed', + 'NftRentalMarketplace: Transfer failed', ) }) it('Should NOT accept a rental offer if lender fee transfer fails', async () => { await mockERC20.transferReverts(true, 2) await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( - 'OriumNftMarketplace: Transfer failed', + 'NftRentalMarketplace: Transfer failed', ) }) it('Should NOT accept a rental offer twice', async () => { await marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration) await expect(marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration)).to.be.revertedWith( - 'OriumNftMarketplace: This offer has an ongoing rental', + 'NftRentalMarketplace: This offer has an ongoing rental', ) }) }) From d85c4da8275ed1dfe97a55643a504730398b7fe6 Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Fri, 19 Apr 2024 17:20:58 -0300 Subject: [PATCH 5/6] ON-812: prettier --- contracts/libraries/LibNftRentalMarketplace.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/libraries/LibNftRentalMarketplace.sol b/contracts/libraries/LibNftRentalMarketplace.sol index 0ed039b..b7ec973 100644 --- a/contracts/libraries/LibNftRentalMarketplace.sol +++ b/contracts/libraries/LibNftRentalMarketplace.sol @@ -91,7 +91,7 @@ library LibNftRentalMarketplace { require(_nonceDeadline == 0, 'NftRentalMarketplace: nonce already used'); } - /** + /** * @dev All values needs to be in wei. * @param _amount The amount to calculate the percentage from. * @param _percentage The percentage to calculate. @@ -100,7 +100,7 @@ library LibNftRentalMarketplace { return (_amount * _percentage) / MAX_PERCENTAGE; } - /** + /** * @notice Transfers the fees. * @dev The fee token address should be approved before calling this function. * @param _feeTokenAddress The fee token address. @@ -212,5 +212,4 @@ library LibNftRentalMarketplace { ); } } - } From b5ae0532f9897fada6cad5d3b33755a85597532f Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Mon, 22 Apr 2024 14:57:55 -0300 Subject: [PATCH 6/6] ON-813: PR fixes --- .../libraries/LibNftRentalMarketplace.sol | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/contracts/libraries/LibNftRentalMarketplace.sol b/contracts/libraries/LibNftRentalMarketplace.sol index b7ec973..d5bf65d 100644 --- a/contracts/libraries/LibNftRentalMarketplace.sol +++ b/contracts/libraries/LibNftRentalMarketplace.sol @@ -106,7 +106,7 @@ library LibNftRentalMarketplace { * @param _feeTokenAddress The fee token address. * @param _marketplaceTreasuryAddress The marketplace treasury address. * @param _lenderAddress The lender address. - * @param _royaltiesAddress The Orium marketplace royalties contract address. + * @param _oriumRoyaltiesAddress The Orium marketplace royalties contract address. * @param _tokenAddress The token address. * @param _feeAmountPerSecond The fee amount per second. * @param _duration The duration of the rental. @@ -115,7 +115,7 @@ library LibNftRentalMarketplace { address _feeTokenAddress, address _marketplaceTreasuryAddress, address _lenderAddress, - address _royaltiesAddress, + address _oriumRoyaltiesAddress, address _tokenAddress, uint256 _feeAmountPerSecond, uint64 _duration @@ -123,7 +123,7 @@ library LibNftRentalMarketplace { uint256 _totalAmount = _feeAmountPerSecond * _duration; if (_totalAmount == 0) return; - IOriumMarketplaceRoyalties _royalties = IOriumMarketplaceRoyalties(_royaltiesAddress); + IOriumMarketplaceRoyalties _royalties = IOriumMarketplaceRoyalties(_oriumRoyaltiesAddress); uint256 _marketplaceFeePercentageInWei = _royalties.marketplaceFeeOf(_tokenAddress); IOriumMarketplaceRoyalties.RoyaltyInfo memory _royaltyInfo = _royalties.royaltyInfoOf(_tokenAddress); @@ -131,24 +131,34 @@ library LibNftRentalMarketplace { uint256 _royaltyAmount = getAmountFromPercentage(_totalAmount, _royaltyInfo.royaltyPercentageInWei); uint256 _lenderAmount = _totalAmount - _royaltyAmount - _marketplaceAmount; - _transferAmount(_feeTokenAddress, msg.sender, _marketplaceTreasuryAddress, _marketplaceAmount); - _transferAmount(_feeTokenAddress, msg.sender, _royaltyInfo.treasury, _royaltyAmount); - _transferAmount(_feeTokenAddress, msg.sender, _lenderAddress, _lenderAmount); + _transferAmount(_feeTokenAddress, _marketplaceTreasuryAddress, _marketplaceAmount); + _transferAmount(_feeTokenAddress, _royaltyInfo.treasury, _royaltyAmount); + _transferAmount(_feeTokenAddress, _lenderAddress, _lenderAmount); } /** * @notice Transfers an amount to a receipient. * @dev This function is used to make an ERC20 transfer. * @param _tokenAddress The token address. - * @param _from The sender address. * @param _to The recipient address. * @param _amount The amount to transfer. */ - function _transferAmount(address _tokenAddress, address _from, address _to, uint256 _amount) internal { + function _transferAmount(address _tokenAddress, address _to, uint256 _amount) internal { if (_amount == 0) return; - require(IERC20(_tokenAddress).transferFrom(_from, _to, _amount), 'NftRentalMarketplace: Transfer failed'); + require(IERC20(_tokenAddress).transferFrom(msg.sender, _to, _amount), 'NftRentalMarketplace: Transfer failed'); } + /** + * @notice Validates the accept rental offer. + * @dev This function is used to validate the accept rental offer params. + * @param _borrower The borrower address + * @param _minDuration The minimum duration of the rental + * @param _isCreated The boolean value to check if the offer is created + * @param _previousRentalExpirationDate The expiration date of the previous rental + * @param _duration The duration of the rental + * @param _nonceDeadline The deadline of the nonce + * @param _expirationDate The expiration date of the rental + */ function validateAcceptRentalOfferParams( address _borrower, uint64 _minDuration, @@ -175,7 +185,7 @@ library LibNftRentalMarketplace { } /** - * @notice grants roles for the same NFT. + * @notice Grants multiple roles to the same NFT. * @dev This function is used to batch grant roles for the same NFT. * @param _oriumMarketplaceRoyalties The Orium marketplace royalties contract address. * @param _tokenAddress The token address.