diff --git a/contracts/protocol/facets/Offer.sol b/contracts/protocol/facets/Offer.sol index 3709d77b..322106c9 100644 --- a/contracts/protocol/facets/Offer.sol +++ b/contracts/protocol/facets/Offer.sol @@ -367,6 +367,9 @@ contract OfferFacet is Context, OfferErrors, Access, FundsManager, IOfferEvents * * Emits VerificationInitiated and ItemPriceObserved events * + * N.B. If unwrapping using selfSale for an offer that was set up with both verifier fee and seller deposit, the seller deposit must be + * paid in advance using the depositFunds method. + * * Reverts if: * - Caller is not the seller's assistant or facilitator * - If seller deposit is non zero and there are not enough funds to cover it @@ -419,7 +422,10 @@ contract OfferFacet is Context, OfferErrors, Access, FundsManager, IOfferEvents EntityLib.validateSellerAssistantOrFacilitator(sellerId, offer.facilitatorId); handleBosonSellerDeposit(sellerId, exchangeToken, offer.sellerDeposit); - // WrapType wrapType = _wrapType; + if (_wrapType != FermionTypes.WrapType.SELF_SALE && offer.sellerDeposit == 0 && msg.value != 0) { + revert FundsErrors.NativeNotAllowed(); + } + deriveAndValidatePriceDiscoveryData(tokenId, _priceDiscovery, exchangeToken, _data); uint256 bosonProtocolFee = getBosonProtocolFee(exchangeToken, _priceDiscovery.price); @@ -497,6 +503,7 @@ contract OfferFacet is Context, OfferErrors, Access, FundsManager, IOfferEvents if (customItemPrice == 0) { revert InvalidCustomItemPrice(); } + if (exchangeAmount > 0) { validateIncomingPayment(exchangeToken, exchangeAmount); transferERC20FromProtocol(exchangeToken, payable(_priceDiscovery.priceDiscoveryContract), exchangeAmount); @@ -585,7 +592,7 @@ contract OfferFacet is Context, OfferErrors, Access, FundsManager, IOfferEvents /** * Handle Boson seller deposit * - * If the seller deposit is non zero, the amount must be deposited into Boson so unwrapping can succed. + * If the seller deposit is non zero, the amount must be deposited into Boson so unwrapping can succeed. * It the seller has some available funds in Fermion, they are used first. * Otherwise, the seller must provide the missing amount. * @@ -609,6 +616,7 @@ contract OfferFacet is Context, OfferErrors, Access, FundsManager, IOfferEvents ]; if (availableFunds >= _sellerDeposit) { + if (_exchangeToken != address(0) && msg.value > 0) revert FundsErrors.NativeNotAllowed(); decreaseAvailableFunds(_sellerId, _exchangeToken, _sellerDeposit); } else { // For offers in native token, the seller deposit cannot be sent at the time of unwrapping. diff --git a/test/protocol/offerFacet.ts b/test/protocol/offerFacet.ts index 300e28ce..226b650b 100644 --- a/test/protocol/offerFacet.ts +++ b/test/protocol/offerFacet.ts @@ -2275,6 +2275,25 @@ describe("Offer", function () { }); }); + it("Native sent to ERC20 offer and seller deposit fully covered", async function () { + await fundsFacet.depositFunds(sellerId, exchangeToken, sellerDeposit); + + await expect( + offerFacet.unwrapNFT(tokenId, WrapType.OS_AUCTION, buyerAdvancedOrder, { value: sellerDeposit }), + ).to.be.revertedWithCustomError(fermionErrors, "NativeNotAllowed"); + }); + + it("Native sent to ERC20 offer and seller deposit partially covered", async function () { + const remainder = sellerDeposit / 10n; + await fundsFacet.depositFunds(sellerId, exchangeToken, sellerDeposit - remainder); + + await mockToken.approve(fermionProtocolAddress, remainder); + + await expect( + offerFacet.unwrapNFT(tokenId, WrapType.OS_AUCTION, buyerAdvancedOrder, { value: sellerDeposit }), + ).to.be.revertedWithCustomError(fermionErrors, "NativeNotAllowed"); + }); + it("Price does not cover the verifier fee", async function () { const minimalPriceNew = calculateMinimalPrice( verifierFee, @@ -3870,6 +3889,14 @@ describe("Offer", function () { const newBosonProtocolBalance = await mockToken.balanceOf(bosonProtocolAddress); expect(newBosonProtocolBalance).to.equal(bosonProtocolBalance + fullPrice - openSeaFee); }); + + context("Revert reasons", function () { + it("Native funds cannot be sent if seller deposit is 0", async function () { + await expect( + offerFacet.unwrapNFT(tokenId, WrapType.OS_AUCTION, buyerAdvancedOrder, { value: parseEther("1") }), + ).to.be.revertedWithCustomError(fermionErrors, "NativeNotAllowed"); + }); + }); }); context("unwrapToSelf", function () { @@ -3939,7 +3966,6 @@ describe("Offer", function () { bosonProtocolBalance = await ethers.provider.getBalance(bosonProtocolAddress); - // await mockToken.approve(fermionProtocolAddress, minimalPrice); const tx = await offerFacet.unwrapNFT(tokenId, WrapType.SELF_SALE, selfSaleData, { value: minimalPrice, }); @@ -3976,6 +4002,12 @@ describe("Offer", function () { .withArgs(minimalPrice, minimalPrice - 1n); await mockToken.setBurnAmount(0); }); + + it("Native funds cannot be sent if seller deposit is 0", async function () { + await expect( + offerFacet.unwrapNFT(tokenId, WrapType.SELF_SALE, selfSaleData, { value: parseEther("1") }), + ).to.be.revertedWithCustomError(fermionErrors, "NativeNotAllowed"); + }); }); }); }); @@ -4122,6 +4154,12 @@ describe("Offer", function () { offerFacet.unwrapNFT(tokenId, WrapType.OS_AUCTION, buyerAdvancedOrder), ).to.be.revertedWithCustomError(fermionErrors, "InvalidUnwrap"); }); + + it("Native funds cannot be sent if seller deposit is 0", async function () { + await expect( + offerFacet.unwrapNFT(tokenId, WrapType.OS_FIXED_PRICE, encodedPrice, { value: parseEther("1") }), + ).to.be.revertedWithCustomError(fermionErrors, "NativeNotAllowed"); + }); }); }); });