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 percentbuyer1
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" );