Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Accept offer to pay with native token MARKETPLACE ERC-1155 #59

Merged
merged 15 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 43 additions & 15 deletions contracts/NftRentalMarketplace.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ 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 { ReentrancyGuardUpgradeable } from '@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol';
import { LibNftRentalMarketplace, RentalOffer, Rental } from './libraries/LibNftRentalMarketplace.sol';

/**
* @title Orium NFT Marketplace - Marketplace for renting NFTs
* @dev This contract is used to manage NFTs rentals, powered by ERC-7432 Non-Fungible Token Roles
* @author Orium Network Team - [email protected]
*/
contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgradeable {
contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable {
/** ######### Global Variables ########### **/

/// @dev oriumMarketplaceRoyalties stores the collection royalties and fees
Expand Down Expand Up @@ -92,6 +93,7 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr
function initialize(address _owner, address _oriumMarketplaceRoyalties) external initializer {
__Pausable_init();
__Ownable_init();
__ReentrancyGuard_init();

oriumMarketplaceRoyalties = _oriumMarketplaceRoyalties;

Expand Down Expand Up @@ -146,7 +148,10 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr
* @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 {
function acceptRentalOffer(
RentalOffer calldata _offer,
uint64 _duration
) external payable whenNotPaused nonReentrant {
bytes32 _offerHash = LibNftRentalMarketplace.hashRentalOffer(_offer);
uint64 _expirationDate = uint64(block.timestamp + _duration);
LibNftRentalMarketplace.validateAcceptRentalOfferParams(
Expand All @@ -159,15 +164,38 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr
_expirationDate
);

LibNftRentalMarketplace.transferFees(
_offer.feeTokenAddress,
owner(),
_offer.lender,
oriumMarketplaceRoyalties,
_offer.tokenAddress,
_offer.feeAmountPerSecond,
_duration
);
if (_offer.feeTokenAddress == address(0)) {
uint256 totalFeeAmount = _offer.feeAmountPerSecond * _duration;
require(msg.value >= totalFeeAmount, 'NftRentalMarketplace: Incorrect native token amount');

uint256 marketplaceFeeAmount = LibNftRentalMarketplace.getAmountFromPercentage(
totalFeeAmount,
IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).marketplaceFeeOf(_offer.tokenAddress)
);
IOriumMarketplaceRoyalties.RoyaltyInfo memory royaltyInfo = IOriumMarketplaceRoyalties(
oriumMarketplaceRoyalties
).royaltyInfoOf(_offer.tokenAddress);

uint256 royaltyAmount = LibNftRentalMarketplace.getAmountFromPercentage(
totalFeeAmount,
royaltyInfo.royaltyPercentageInWei
);
uint256 lenderAmount = totalFeeAmount - marketplaceFeeAmount - royaltyAmount;
EduardoMelo00 marked this conversation as resolved.
Show resolved Hide resolved

payable(owner()).transfer(marketplaceFeeAmount);
payable(royaltyInfo.treasury).transfer(royaltyAmount);
payable(_offer.lender).transfer(lenderAmount);
} else {
LibNftRentalMarketplace.transferFees(
_offer.feeTokenAddress,
owner(),
_offer.lender,
oriumMarketplaceRoyalties,
_offer.tokenAddress,
_offer.feeAmountPerSecond,
_duration
);
}

LibNftRentalMarketplace.grantRoles(
oriumMarketplaceRoyalties,
Expand All @@ -180,8 +208,8 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr
);

for (uint256 i = 0; i < _offer.roles.length; i++) {
if(_expirationDate > roleDeadline[_offer.roles[i]][_offer.tokenAddress][_offer.tokenId]) {
roleDeadline[_offer.roles[i]][_offer.tokenAddress][_offer.tokenId] = _expirationDate;
if (_expirationDate > roleDeadline[_offer.roles[i]][_offer.tokenAddress][_offer.tokenId]) {
roleDeadline[_offer.roles[i]][_offer.tokenAddress][_offer.tokenId] = _expirationDate;
}
}

Expand Down Expand Up @@ -233,7 +261,7 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr
_offer.tokenId,
_offer.roles
);

uint64 _offerDeadline = nonceDeadline[_offer.lender][_offer.nonce];
if (_offerDeadline < uint64(block.timestamp)) {
for (uint256 i = 0; i < _offer.roles.length; i++) {
Expand Down Expand Up @@ -268,6 +296,7 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr
* @dev owner will be msg.sender and it must approve the marketplace to revoke the roles.
* @param _tokenAddresses The array of tokenAddresses
* @param _tokenIds The array of tokenIds
* @param _roleIds The array of roleIds
*/
function batchRevokeRole(
address[] memory _tokenAddresses,
Expand All @@ -294,7 +323,6 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr

nonceDeadline[msg.sender][_offer.nonce] = uint64(block.timestamp);
for (uint256 i = 0; i < _offer.roles.length; i++) {

if (rentals[_offerHash].expirationDate > uint64(block.timestamp)) {
roleDeadline[_offer.roles[i]][_offer.tokenAddress][_offer.tokenId] = rentals[_offerHash].expirationDate;
} else {
Expand Down
37 changes: 24 additions & 13 deletions contracts/OriumSftMarketplace.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity 0.8.9;
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 { ReentrancyGuardUpgradeable } from '@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol';
import { IERC1155 } from '@openzeppelin/contracts/token/ERC1155/IERC1155.sol';
import { IERC7589 } from './interfaces/IERC7589.sol';
import { IERC7589Legacy } from './interfaces/IERC7589Legacy.sol';
Expand All @@ -16,7 +17,7 @@ import { IOriumMarketplaceRoyalties } from './interfaces/IOriumMarketplaceRoyalt
* @dev This contract is used to manage SFTs rentals, powered by ERC-7589 Semi-Fungible Token Roles
* @author Orium Network Team - [email protected]
*/
contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgradeable {
contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable {
/** ######### Global Variables ########### **/

/// @dev oriumMarketplaceRoyalties stores the collection royalties and fees
Expand Down Expand Up @@ -110,6 +111,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra
function initialize(address _owner, address _oriumMarketplaceRoyalties) public initializer {
__Pausable_init();
__Ownable_init();
__ReentrancyGuard_init();

oriumMarketplaceRoyalties = _oriumMarketplaceRoyalties;

Expand All @@ -124,7 +126,6 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra
* @dev To optimize for gas, only the offer hash is stored on-chain
* @param _offer The rental offer struct.
*/

function createRentalOffer(RentalOffer memory _offer) external whenNotPaused {
address _rolesRegistryAddress = IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(
_offer.tokenAddress
Expand Down Expand Up @@ -168,8 +169,10 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra
* @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 {
function acceptRentalOffer(
RentalOffer calldata _offer,
uint64 _duration
) external payable whenNotPaused nonReentrant {
bytes32 _offerHash = LibOriumSftMarketplace.hashRentalOffer(_offer);
uint64 _expirationDate = uint64(block.timestamp + _duration);
LibOriumSftMarketplace.validateAcceptRentalOffer(
Expand Down Expand Up @@ -441,15 +444,22 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra
);
uint256 _lenderAmount = _feeAmount - _royaltyAmount - _marketplaceFeeAmount;

LibOriumSftMarketplace.transferFees(
_feeTokenAddress,
_marketplaceFeeAmount,
_royaltyAmount,
_lenderAmount,
owner(),
_royaltyInfo.treasury,
_lenderAddress
);
if (_feeTokenAddress == address(0)) {
require(msg.value >= _feeAmount, 'OriumSftMarketplace: Insufficient native token amount');
EduardoMelo00 marked this conversation as resolved.
Show resolved Hide resolved
payable(owner()).transfer(_marketplaceFeeAmount);
payable(_royaltyInfo.treasury).transfer(_royaltyAmount);
payable(_lenderAddress).transfer(_lenderAmount);
} else {
LibOriumSftMarketplace.transferFees(
_feeTokenAddress,
_marketplaceFeeAmount,
_royaltyAmount,
_lenderAmount,
owner(),
_royaltyInfo.treasury,
_lenderAddress
);
}
}

/**
Expand Down Expand Up @@ -496,5 +506,6 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra
function setOriumMarketplaceRoyalties(address _oriumMarketplaceRoyalties) external onlyOwner {
oriumMarketplaceRoyalties = _oriumMarketplaceRoyalties;
}

/** ######### Getters ########### **/
}
48 changes: 48 additions & 0 deletions test/NftRentalMarketplace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ describe('NftRentalMarketplace', () => {
.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

Expand Down Expand Up @@ -451,6 +452,50 @@ describe('NftRentalMarketplace', () => {
marketplace.connect(notOperator).acceptRentalOffer(rentalOffer, duration),
).to.be.revertedWith('NftRentalMarketplace: Sender is not allowed to rent this NFT')
})
it('Should revert when accepting a rental offer with insufficient native tokens', 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
rentalOffer.feeTokenAddress = AddressZero
rentalOffer.feeAmountPerSecond = toWei('0.0000001')
await marketplaceRoyalties
.connect(operator)
.setTrustedFeeTokenForToken([rentalOffer.tokenAddress], [AddressZero], [true])
await marketplace.connect(lender).createRentalOffer(rentalOffer)

const totalFeeAmount = rentalOffer.feeAmountPerSecond * BigInt(duration)

const insufficientAmount = totalFeeAmount - toWei('0.00000001') // slightly less than required
await expect(
marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration, {
value: insufficientAmount.toString(),
}),
).to.be.revertedWith('NftRentalMarketplace: Incorrect native token amount')
})
it('Should accept a rental offer with native tokens', 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
rentalOffer.feeTokenAddress = AddressZero
rentalOffer.feeAmountPerSecond = toWei('0.0000001')
await marketplaceRoyalties
.connect(operator)
.setTrustedFeeTokenForToken([rentalOffer.tokenAddress], [AddressZero], [true])
await marketplace.connect(lender).createRentalOffer(rentalOffer)

const blockTimestamp = await time.latest()
const expirationDate = blockTimestamp + duration + 1

await expect(
marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration, {
value: totalFeeAmount.toString(),
}),
)
.to.emit(marketplace, 'RentalStarted')
.withArgs(rentalOffer.lender, rentalOffer.nonce, borrower.address, expirationDate)
})
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()
Expand All @@ -473,6 +518,7 @@ describe('NftRentalMarketplace', () => {
marketplace.connect(borrower).acceptRentalOffer(rentalOffer, maxDuration),
).to.be.revertedWith('NftRentalMarketplace: expiration date is greater than offer deadline')
})

describe('Fees', async function () {
const feeAmountPerSecond = toWei('1')
const feeAmount = feeAmountPerSecond * BigInt(duration)
Expand All @@ -495,6 +541,7 @@ describe('NftRentalMarketplace', () => {
.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)
Expand All @@ -504,6 +551,7 @@ describe('NftRentalMarketplace', () => {
'RentalStarted',
)
})

it('Should accept a rental offer if royalty fee is 0', async () => {
await marketplaceRoyalties
.connect(creator)
Expand Down
50 changes: 49 additions & 1 deletion test/OriumSftMarketplace.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable no-unexpected-multiline */
// SPDX-License-Identifier: CC0-1.0

import { ethers } from 'hardhat'
import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers'
import { deploySftMarketplaceContracts } from './fixtures/OriumSftMarketplaceFixture'
Expand All @@ -15,6 +16,7 @@
OriumSftMarketplace,
SftRolesRegistrySingleRole,
SftRolesRegistrySingleRoleLegacy,
ReentrancyAttack,

Check warning on line 19 in test/OriumSftMarketplace.test.ts

View workflow job for this annotation

GitHub Actions / build_test_deploy

'ReentrancyAttack' is defined but never used
} from '../typechain-types'

describe('OriumSftMarketplace', () => {
Expand Down Expand Up @@ -143,6 +145,7 @@
.connect(lender)
.setApprovalForAll(await SftRolesRegistrySingleRoleLegacy.getAddress(), true)
})

describe('When Rental Offer is not created', async () => {
describe('Create Rental Offer', async () => {
describe("When commitmentId doesn't exist", async () => {
Expand Down Expand Up @@ -674,6 +677,51 @@
marketplace.connect(borrower).acceptRentalOffer(rentalOffer, maxDuration),
).to.be.revertedWith('OriumSftMarketplace: expiration date is greater than offer deadline')
})
// New test case for accepting rental offer with native tokens
it('Should accept a rental offer with native tokens', async () => {
await marketplaceRoyalties
.connect(operator)
.setTrustedFeeTokenForToken([rentalOffer.tokenAddress], [AddressZero], [true])

rentalOffer.feeTokenAddress = AddressZero
rentalOffer.feeAmountPerSecond = toWei('0.0000001')
totalFeeAmount = rentalOffer.feeAmountPerSecond * BigInt(duration)
rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}`
await marketplace.connect(lender).createRentalOffer({ ...rentalOffer, commitmentId: BigInt(0) })
rentalOffer.commitmentId = BigInt(2)

const blockTimestamp = (await ethers.provider.getBlock('latest'))?.timestamp
const expirationDate = Number(blockTimestamp) + duration + 1

await expect(
marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration, {
value: totalFeeAmount.toString(),
}),
)
.to.emit(marketplace, 'RentalStarted')
.withArgs(rentalOffer.lender, rentalOffer.nonce, borrower.address, expirationDate)
})

it('Should revert when accepting a rental offer with insufficient native tokens', async function () {
await marketplaceRoyalties
.connect(operator)
.setTrustedFeeTokenForToken([rentalOffer.tokenAddress], [AddressZero], [true])

rentalOffer.feeTokenAddress = AddressZero
rentalOffer.feeAmountPerSecond = toWei('0.0000001')
const totalFeeAmount = rentalOffer.feeAmountPerSecond * BigInt(duration)
rentalOffer.nonce = `0x${randomBytes(32).toString('hex')}`
await marketplace.connect(lender).createRentalOffer({ ...rentalOffer, commitmentId: BigInt(0) })
rentalOffer.commitmentId = BigInt(2)

const insufficientAmount = totalFeeAmount - BigInt(toWei('0.00000001')) // slightly less than required
await expect(
marketplace.connect(borrower).acceptRentalOffer(rentalOffer, BigInt(duration), {
value: insufficientAmount.toString(),
}),
).to.be.revertedWith('OriumSftMarketplace: Insufficient native token amount')
})

describe('Fees', async function () {
const feeAmountPerSecond = toWei('1')
const feeAmount = feeAmountPerSecond * BigInt(duration)
Expand Down
Loading