diff --git a/.openzeppelin/polygon.json b/.openzeppelin/polygon.json index 0a35727..6a0989e 100644 --- a/.openzeppelin/polygon.json +++ b/.openzeppelin/polygon.json @@ -4030,6 +4030,189 @@ }, "namespaces": {} } + }, + "27b07b4cb603b26fff3ef1cbbfd39da1d0266a774fac0884ae1c2ed6caacbf7a": { + "address": "0x8aFfF4377E66782287dd08e6C793eaD674D9685D", + "txHash": "0x56200589431d4b902765fbdaefe35aac263281e2d7a543d1f22cf86db5f27487", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "oriumMarketplaceRoyalties", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "OriumSftMarketplace", + "src": "contracts/OriumSftMarketplace.sol:23" + }, + { + "label": "isCreated", + "offset": 0, + "slot": "152", + "type": "t_mapping(t_bytes32,t_bool)", + "contract": "OriumSftMarketplace", + "src": "contracts/OriumSftMarketplace.sol:29" + }, + { + "label": "nonceDeadline", + "offset": 0, + "slot": "153", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint64))", + "contract": "OriumSftMarketplace", + "src": "contracts/OriumSftMarketplace.sol:32" + }, + { + "label": "commitmentIdToNonce", + "offset": 0, + "slot": "154", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))", + "contract": "OriumSftMarketplace", + "src": "contracts/OriumSftMarketplace.sol:35" + }, + { + "label": "rentals", + "offset": 0, + "slot": "155", + "type": "t_mapping(t_bytes32,t_struct(Rental)7265_storage)", + "contract": "OriumSftMarketplace", + "src": "contracts/OriumSftMarketplace.sol:38" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint256))": { + "label": "mapping(address => mapping(uint256 => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint64))": { + "label": "mapping(address => mapping(uint256 => uint64))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bool)": { + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(Rental)7265_storage)": { + "label": "mapping(bytes32 => struct OriumSftMarketplace.Rental)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint64)": { + "label": "mapping(uint256 => uint64)", + "numberOfBytes": "32" + }, + "t_struct(Rental)7265_storage": { + "label": "struct OriumSftMarketplace.Rental", + "members": [ + { + "label": "borrower", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "expirationDate", + "type": "t_uint64", + "offset": 20, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } diff --git a/.openzeppelin/unknown-1284.json b/.openzeppelin/unknown-1284.json index 2311d06..028ad05 100644 --- a/.openzeppelin/unknown-1284.json +++ b/.openzeppelin/unknown-1284.json @@ -1111,6 +1111,185 @@ }, "namespaces": {} } + }, + "d30893d8e86922c92e4b3e9681cb8395730160aa4da35e933bb6308357c89532": { + "address": "0x558eb7a37CAEE6d6F151312dA00FADb8d25937AE", + "txHash": "0xd076bb6cc3366cdbf01100409036519266e1589f66f0851fe83cf7066e88fdf3", + "layout": { + "solcVersion": "0.8.9", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "oriumMarketplaceRoyalties", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "NftRentalMarketplace", + "src": "contracts/NftRentalMarketplace.sol:23" + }, + { + "label": "isCreated", + "offset": 0, + "slot": "152", + "type": "t_mapping(t_bytes32,t_bool)", + "contract": "NftRentalMarketplace", + "src": "contracts/NftRentalMarketplace.sol:26" + }, + { + "label": "nonceDeadline", + "offset": 0, + "slot": "153", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint64))", + "contract": "NftRentalMarketplace", + "src": "contracts/NftRentalMarketplace.sol:29" + }, + { + "label": "roleDeadline", + "offset": 0, + "slot": "154", + "type": "t_mapping(t_bytes32,t_mapping(t_address,t_mapping(t_uint256,t_uint64)))", + "contract": "NftRentalMarketplace", + "src": "contracts/NftRentalMarketplace.sol:32" + }, + { + "label": "rentals", + "offset": 0, + "slot": "155", + "type": "t_mapping(t_bytes32,t_struct(Rental)9028_storage)", + "contract": "NftRentalMarketplace", + "src": "contracts/NftRentalMarketplace.sol:35" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint64))": { + "label": "mapping(address => mapping(uint256 => uint64))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_bool)": { + "label": "mapping(bytes32 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_mapping(t_address,t_mapping(t_uint256,t_uint64)))": { + "label": "mapping(bytes32 => mapping(address => mapping(uint256 => uint64)))", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(Rental)9028_storage)": { + "label": "mapping(bytes32 => struct Rental)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint64)": { + "label": "mapping(uint256 => uint64)", + "numberOfBytes": "32" + }, + "t_struct(Rental)9028_storage": { + "label": "struct Rental", + "members": [ + { + "label": "borrower", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "expirationDate", + "type": "t_uint64", + "offset": 20, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } diff --git a/addresses/moonbeam/index.json b/addresses/moonbeam/index.json index 8625b84..abdcf05 100644 --- a/addresses/moonbeam/index.json +++ b/addresses/moonbeam/index.json @@ -35,10 +35,10 @@ "NftRentalMarketplace": { "address": "0x201E1636BB21Dfd51F93815BCD008EAe2Fa29bD9", "operator": "0x04c8c6c56dab836f8bd62cb6884371507e706806", - "implementation": "0x830cbc0e100e72e8682391a668ce6c9db703848f", + "implementation": "0x558eb7a37CAEE6d6F151312dA00FADb8d25937AE", "proxyAdmin": "0x668e73cF24361cfE13801681d8885e6632A7Eaa6", "libraries": { - "LibNftRentalMarketplace": "0x4BDb2d8f3833b38307dfC19231dC7cD65b59504a" + "LibNftRentalMarketplace": "0x89aD0a3E1F72eb14281Ec02Fdf5dcf4FB5e01845" } } } \ No newline at end of file diff --git a/addresses/polygon/index.json b/addresses/polygon/index.json index d53134d..aa87c0b 100644 --- a/addresses/polygon/index.json +++ b/addresses/polygon/index.json @@ -29,10 +29,10 @@ "OriumSftMarketplace": { "address": "0xB1D47B09aa6D81d7B00C3A37705a6A157B83C49F", "operator": "0x359E1208DE02Af11461A37D72165Ef2dcD2Adfc8", - "implementation": "0x24249E232b49740e940d1CE04Ce5B0b50e8dAeF8", + "implementation": "0x8aFfF4377E66782287dd08e6C793eaD674D9685D", "proxyAdmin": "0x48c769f6a8de57d824f0e7330d7A27dee53a43cD", "libraries": { - "LibOriumSftMarketplace": "0x7D037fCfD9147c2182b50ec4FeEf0231F91DaA3d" + "LibOriumSftMarketplace": "0x71C24d086FF041C86CEA2737fd70cA5F1a400f23" } }, "ERC7432WrapperForERC4907": { diff --git a/contracts/NftRentalMarketplace.sol b/contracts/NftRentalMarketplace.sol index cf94158..f2f07ff 100644 --- a/contracts/NftRentalMarketplace.sol +++ b/contracts/NftRentalMarketplace.sol @@ -146,7 +146,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 { bytes32 _offerHash = LibNftRentalMarketplace.hashRentalOffer(_offer); uint64 _expirationDate = uint64(block.timestamp + _duration); LibNftRentalMarketplace.validateAcceptRentalOfferParams( @@ -159,16 +162,6 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr _expirationDate ); - LibNftRentalMarketplace.transferFees( - _offer.feeTokenAddress, - owner(), - _offer.lender, - oriumMarketplaceRoyalties, - _offer.tokenAddress, - _offer.feeAmountPerSecond, - _duration - ); - LibNftRentalMarketplace.grantRoles( oriumMarketplaceRoyalties, _offer.tokenAddress, @@ -180,12 +173,13 @@ 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; } } rentals[_offerHash] = Rental({ borrower: msg.sender, expirationDate: _expirationDate }); + _transferFees(_offer.tokenAddress, _offer.feeTokenAddress, _offer.feeAmountPerSecond, _duration, _offer.lender); emit RentalStarted(_offer.lender, _offer.nonce, msg.sender, _expirationDate); } @@ -233,7 +227,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++) { @@ -268,6 +262,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, @@ -294,7 +289,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 { @@ -304,6 +298,54 @@ contract NftRentalMarketplace is Initializable, OwnableUpgradeable, PausableUpgr emit RentalOfferCancelled(_offer.lender, _offer.nonce); } + /** + * @dev Transfers the fees to the marketplace, the creator and the lender. + * @param _feeTokenAddress The address of the ERC20 token for rental fees. + * @param _feeAmountPerSecond The amount of fee per second. + * @param _duration The duration of the rental. + * @param _lenderAddress The address of the lender. + */ + function _transferFees( + address _tokenAddress, + address _feeTokenAddress, + uint256 _feeAmountPerSecond, + uint64 _duration, + address _lenderAddress + ) internal { + if (_feeTokenAddress == address(0)) { + uint256 totalFeeAmount = _feeAmountPerSecond * _duration; + require(msg.value == totalFeeAmount, 'NftRentalMarketplace: Incorrect native token amount'); + + uint256 marketplaceFeeAmount = LibNftRentalMarketplace.getAmountFromPercentage( + totalFeeAmount, + IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).marketplaceFeeOf(_tokenAddress) + ); + IOriumMarketplaceRoyalties.RoyaltyInfo memory royaltyInfo = IOriumMarketplaceRoyalties( + oriumMarketplaceRoyalties + ).royaltyInfoOf(_tokenAddress); + + uint256 royaltyAmount = LibNftRentalMarketplace.getAmountFromPercentage( + totalFeeAmount, + royaltyInfo.royaltyPercentageInWei + ); + uint256 lenderAmount = totalFeeAmount - marketplaceFeeAmount - royaltyAmount; + + payable(owner()).transfer(marketplaceFeeAmount); + payable(royaltyInfo.treasury).transfer(royaltyAmount); + payable(_lenderAddress).transfer(lenderAmount); + } else { + LibNftRentalMarketplace.transferFees( + _feeTokenAddress, + owner(), + _lenderAddress, + oriumMarketplaceRoyalties, + _tokenAddress, + _feeAmountPerSecond, + _duration + ); + } + } + /** ============================ Core Functions ================================== **/ /** ######### Setters ########### **/ diff --git a/contracts/OriumSftMarketplace.sol b/contracts/OriumSftMarketplace.sol index 41fb470..0ddf6b7 100644 --- a/contracts/OriumSftMarketplace.sol +++ b/contracts/OriumSftMarketplace.sol @@ -124,7 +124,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 @@ -168,8 +167,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 { bytes32 _offerHash = LibOriumSftMarketplace.hashRentalOffer(_offer); uint64 _expirationDate = uint64(block.timestamp + _duration); LibOriumSftMarketplace.validateAcceptRentalOffer( @@ -182,8 +183,6 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra _expirationDate ); - _transferFees(_offer.tokenAddress, _offer.feeTokenAddress, _offer.feeAmountPerSecond, _duration, _offer.lender); - IERC7589 _rolesRegistry = IERC7589( IOriumMarketplaceRoyalties(oriumMarketplaceRoyalties).sftRolesRegistryOf(_offer.tokenAddress) ); @@ -199,6 +198,7 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra } rentals[_offerHash] = Rental({ borrower: msg.sender, expirationDate: _expirationDate }); + _transferFees(_offer.tokenAddress, _offer.feeTokenAddress, _offer.feeAmountPerSecond, _duration, _offer.lender); emit RentalStarted(_offer.lender, _offer.nonce, msg.sender, _expirationDate); } @@ -441,15 +441,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'); + payable(owner()).transfer(_marketplaceFeeAmount); + payable(_royaltyInfo.treasury).transfer(_royaltyAmount); + payable(_lenderAddress).transfer(_lenderAmount); + } else { + LibOriumSftMarketplace.transferFees( + _feeTokenAddress, + _marketplaceFeeAmount, + _royaltyAmount, + _lenderAmount, + owner(), + _royaltyInfo.treasury, + _lenderAddress + ); + } } /** @@ -496,5 +503,6 @@ contract OriumSftMarketplace is Initializable, OwnableUpgradeable, PausableUpgra function setOriumMarketplaceRoyalties(address _oriumMarketplaceRoyalties) external onlyOwner { oriumMarketplaceRoyalties = _oriumMarketplaceRoyalties; } + /** ######### Getters ########### **/ } diff --git a/contracts/mocks/ReentrancyAttack.sol b/contracts/mocks/ReentrancyAttack.sol new file mode 100644 index 0000000..4417f6c --- /dev/null +++ b/contracts/mocks/ReentrancyAttack.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import '../OriumSftMarketplace.sol'; + +contract ReentrancyAttack { + OriumSftMarketplace public marketplace; + RentalOffer public offer; + uint64 public duration; + + constructor(OriumSftMarketplace _marketplace) { + marketplace = _marketplace; + } + + receive() external payable { + marketplace.acceptRentalOffer{ value: msg.value }(offer, duration); + } + + function attack(RentalOffer calldata _offer, uint64 _duration) external payable { + offer = _offer; + duration = _duration; + + marketplace.acceptRentalOffer{ value: msg.value }(_offer, _duration); + } +} diff --git a/test/NftRentalMarketplace.test.ts b/test/NftRentalMarketplace.test.ts index 0677f83..166e339 100644 --- a/test/NftRentalMarketplace.test.ts +++ b/test/NftRentalMarketplace.test.ts @@ -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 @@ -451,6 +452,51 @@ 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 totalFeeAmount = rentalOffer.feeAmountPerSecond * BigInt(duration) + + const blockTimestamp = await time.latest() + const expirationDate = blockTimestamp + duration + 1 + + await expect( + marketplace.connect(borrower).acceptRentalOffer(rentalOffer, duration, { + value: totalFeeAmount, + }), + ) + .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() @@ -473,6 +519,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) @@ -495,6 +542,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) @@ -504,6 +552,7 @@ describe('NftRentalMarketplace', () => { 'RentalStarted', ) }) + it('Should accept a rental offer if royalty fee is 0', async () => { await marketplaceRoyalties .connect(creator) diff --git a/test/OriumSftMarketplace.test.ts b/test/OriumSftMarketplace.test.ts index 1533daf..ac6a9df 100644 --- a/test/OriumSftMarketplace.test.ts +++ b/test/OriumSftMarketplace.test.ts @@ -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' @@ -15,6 +16,7 @@ import { OriumSftMarketplace, SftRolesRegistrySingleRole, SftRolesRegistrySingleRoleLegacy, + ReentrancyAttack, } from '../typechain-types' describe('OriumSftMarketplace', () => { @@ -26,6 +28,7 @@ describe('OriumSftMarketplace', () => { let secondMockERC1155: MockERC1155 let wearableToken: MockERC1155 let mockERC20: MockERC20 + let attackContract: ReentrancyAttack // We are disabling this rule because hardhat uses first account as deployer by default, and we are separating deployer and operator // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -143,6 +146,7 @@ describe('OriumSftMarketplace', () => { .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 () => { @@ -674,6 +678,51 @@ describe('OriumSftMarketplace', () => { 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 accept 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)