//
Discount percent can be greater than BASE
montecristo profile imagemontecristo
Medium

Finding description and impact

Root Cause

There's not enough validation done in SecondSwap_Marketplace.listVesting function:

// @audit _discountPct can be greater than BASE require( (_discountType != DiscountType.NO && _discountPct > 0) || (_discountType == DiscountType.NO), "SS_Marketplace: Invalid discount amount" );

When discount type is FIXED or LINEAR, it will accept any discountPct greater than zero.

Impact

If a discountPct is greater than 100%, there are two different scenairos based on discount type:

When discount type is FIXED

No one can purchase listed vesting because the following line will revert with underflow panic error

} else if (listing.discountType == DiscountType.FIX) { discountedPrice = (discountedPrice * (BASE - listing.discountPct)) / BASE; }

When discount type is LINEAR

Users can purchase part of the listing but no more than listing.total * BASE / listing.discountPct because the following line will revert will underflow:

if (listing.discountType == DiscountType.LINEAR) { discountedPrice = (discountedPrice * (BASE - ((_amount * listing.discountPct) / listing.total))) / BASE; }

Both behaviors fall into the following category of attack ideas (where to focus bugs)

Would the transaction be reverted if there are any conflicting parameters for discounts?

Proof of Concept

PoC examines the following scenairo:

  • user1 has listed vesting with 100.01% FIXED discount percent
  • buyer1 wants to buy 10 tokens but cannot because the transaction reverts with panic error

Apply the following patch:

diff --git a/test/Marketplace.test.ts b/test/Marketplace.test.ts
index e30fd16..9c94792 100644
--- a/test/Marketplace.test.ts
+++ b/test/Marketplace.test.ts
@@ -170,6 +170,43 @@ describe("SecondSwap Marketplace Upgrades", function() {
             }
         });
 
+        it.only("should list partial fill public vesting lot with discount more than 100%", async function () {
+            const { vesting, token, alice, manager, marketplace, user1 } = await loadFixture(deployProxyFixture);
+
+            const amount = parseEther("10");
+            const cost = parseEther("200");
+            const discountPct = BigInt(10001); // Greater than BASE
+            const discountType = 2; // Fix Discount
+            const listingType = 0;
+            const maxWhitelist = BigInt(0);
+            const privateListing = false;
+            const currency = token.address
+            const minPurchaseAmt = BigInt(1)
+
+            await token.write.approve([marketplace.address, parseEther("1000")], { account: user1.account });
+            await marketplace.write.listVesting([vesting.address, amount, cost, discountPct, listingType, discountType, maxWhitelist, currency, minPurchaseAmt, privateListing], { account: user1.account });
+            const listing = await marketplace.read.listings([vesting.address, BigInt(0)]);
+            expect(listing[6]).to.equal(discountPct); // discountPct accepted
+
+            const [buyer1, referrer] = await hre.viem.getWalletClients();
+            await token.write.mint([buyer1.account.address, parseEther("1000")]);
+            await token.write.approve([marketplace.address, parseEther("1000")], { account: buyer1.account });
+            const purchaseAmount = parseEther("10"); // 10 tokens per purchase
+            /**
+             * Details: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block)
+             * Version: viem@2.21.48
+             * at delay.count.count (node_modules/viem/utils/buildRequest.ts:186:25)
+             * at async attemptRetry (node_modules/viem/utils/promise/withRetry.ts:44:22)
+             * Caused by: Error: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block)
+             * at SecondSwap_Marketplace._getDiscountedPrice (contracts/SecondSwap_Marketplace.sol:421)
+             */
+            await expect(marketplace.write.spotPurchase([
+                vesting.address,
+                BigInt(0),
+                purchaseAmount,
+                referrer.account.address
+            ], { account: buyer1.account })).to.be.revertedWithPanic(0x11);
+        });
       });
     
     // Tested

and run the following command:

npx hardhat test test/Marketplace.test.ts

Recommended mitigation steps

The following check could be added:

require( - (_discountType != DiscountType.NO && _discountPct > 0) || (_discountType == DiscountType.NO), + (_discountType != DiscountType.NO && _discountPct > 0 && _discountPct < BASE) || (_discountType == DiscountType.NO), "SS_Marketplace: Invalid discount amount" );