Missing Upper Bound on Discount Percentage, Rendering Listings Ineffective
The SecondSwap_Marketplace::listVesting
function lacks an upper bound validation for the discountPct
parameter, allowing values greater than 100% (10,000 basis points). This can render listings ineffective, as any purchase attempt triggers an underflow error during price calculation, causing the transaction to revert. For example, setting a discount of 150% creates a negative or zero discounted price, violating contract assumptions and breaking listing functionality.
Additionally, the contract does not permit sellers to modify the discountPct
of an existing listing. If an invalid discount is set, then sellers either have to wait for an s2Admin
to manually delist the listing, delist the listing early by paying panelties or wait for the min listing period to be over before delising it which leads to operational inefficiencies and poor user experience.
/// contracts/SecondSwap_Marketplace.sol contract SecondSwap_Marketplace{ /// ... function listVesting( address _vestingPlan, uint256 _amount, uint256 _price, uint256 _discountPct, ListingType _listingType, DiscountType _discountType, uint256 _maxWhitelist, address _currency, uint256 _minPurchaseAmt, bool _isPrivate ) external isFreeze { require( _listingType != ListingType.SINGLE || (_minPurchaseAmt > 0 && _minPurchaseAmt <= _amount), "SS_Marketplace: Minimum Purchase Amount cannot be more than listing amount" ); require(_price > 0, "SS_Marketplace: Price must be greater than 0"); require( @> (_discountType != DiscountType.NO && _discountPct > 0) || (_discountType == DiscountType.NO), "SS_Marketplace: Invalid discount amount" ); } /// .... }
Proof of Concept
The following test demonstrates the addition of a listing with a FIXED
discount and 150%
discount percent. The listing is added successfully but any purchase from the listing reverts with error.
it("should prove allowance of discount > 100%", async function () { const { vesting, token, marketplace, marketplaceSetting, user1, manager, } = await loadFixture(deployProxyFixture) const [user2, user3] = await hre.viem.getWalletClients() // Test parameters const amount = parseEther("100") const cost = parseEther("100") const discountPct = BigInt(15000) // 150% discount const listingType = 1 // Single fill const discountType = 2 // FIXED discount const maxWhitelist = BigInt(0) const privateListing = false const minPurchaseAmt = parseEther("1") const updatedSettings = await manager.read.vestingSettings([ vesting.address, ]) expect(updatedSettings[0]).to.be.true // Check if sellable is true console.log("###################### LISTING ######################") // List vesting await marketplace.write.listVesting( [ vesting.address, amount, cost, discountPct, listingType, discountType, maxWhitelist, token.address, minPurchaseAmt, privateListing, ], { account: user1.account } ) console.log("") console.log("") console.log("") console.log( "###################### VERIFYING LISTING ######################" ) // Verify listing details const listing = await marketplace.read.listings([ vesting.address, BigInt(0), ]) expect(listing[0].toLowerCase()).to.equal(user1.account.address) expect(listing[1]).to.equal(amount) expect(listing[3]).to.equal(cost) expect(listing[5]).to.equal(2) expect(listing[6]).to.equal(discountPct) console.log("seller: ", listing[0]) console.log("amount: ", listing[1]) console.log("price: ", listing[3]) console.log("discountType: ", listing[5]) console.log("discountPercentage: ", listing[6]) console.log("") console.log("") console.log("") // Setup buyer await token.write.mint([user3.account.address, parseEther("10000")]) console.log( "###################### LISTING PURCHASE ######################" ) // Make purchase await marketplace.write.spotPurchase( [ vesting.address, BigInt(0), amount, "0x0000000000000000000000000000000000000000", ], { account: user3.account } ) })
Logs
SecondSwap Marketplace Upgrades C4Audit ###################### LISTING ###################### ###################### VERIFYING LISTING ###################### seller: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 amount: 100000000000000000000n price: 100000000000000000000n discountType: 2 discountPercentage: 15000n ###################### LISTING PURCHASE ###################### 1) should prove allowance of discount > 100% 0 passing (773ms) 1 failing 1) SecondSwap Marketplace Upgrades C4Audit should prove allowance of discount > 100%: ContractFunctionExecutionError: An unknown RPC error occurred. Request Arguments: from: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 to: 0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6 data: 0x0fe2bce3000000000000000000000000ba12646cc07adbe.... Contract Call: address: 0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6 function: spotPurchase(address _vestingPlan, uint256 _listingId, uint256 _amount, address _referral) args: (0xBA12646CC07ADBe43F8bD25D83FB628D29C8A762, 0, 100000000000000000000, 0x0000000000000000000000000000000000000000) sender: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 Docs: https://viem.sh/docs/contract/writeContract Details: VM Exception while processing transaction: reverted with panic code 0x11 (Arithmetic operation overflowed outside of an unchecked block)
Recommended mitigation steps
To migtate this issue an upper bound on the discountPct
should be added
function listVesting(...){ uint256 constant BASE = 10_000 ///... - require(_discountType != DiscountType.NO && _discountPct > 0) - || (_discountType == DiscountType.NO), "SS_Marketplace: Invalid discount amount"); + require(_discountType != DiscountType.NO && _discountPct > 0 && _discountPct <= BASE) + || (_discountType == DiscountType.NO), "SS_Marketplace: Invalid discount amount"); ///... }