Early rate$2,400 of senior audit time for $500. Early members keep the rate as it climbs.$2,400 of senior audit time for $500See how →
F-2026-0005·missing-validation

setInitialFloor has no upper bound and no inverse, so one mis-scaled call permanently freezes the never-traded Special inventory

Fixednfterc-1155marketplace
TL;DR

The opening floor for never-traded Special badges is raise-only with no upper bound and no inverse. One mis-scaled call would put the entire unsold Special inventory permanently beyond any buyer's reach, with no role able to undo it.

Severity
MEDIUM
Impact
MEDIUM
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
MEDIUM
02Section · Description

Description

_setInitialFloor rejects zero and rejects any value below the current one, and imposes no upper bound:

solidity
function _setInitialFloor(uint256 newInitialFloor) internal {
if (newInitialFloor == 0) revert ZeroInitialFloor();
uint256 current = initialFloor;
if (newInitialFloor < current) revert InitialFloorBelowCurrent(newInitialFloor, current);
initialFloor = newInitialFloor;
emit InitialFloorUpdated(newInitialFloor);
}

Every other economic parameter in the codebase is bounded above by an immutable and is freely reversible — feeBps by FEE_BPS_CEILING, flatMinFee by flatMinCeiling, profitBps and commissionBps by their own ceilings. This one has neither.

currentFloor returns initialFloor for every badge whose _floorOf is still 0, so a raise applies retroactively to the entire never-traded Special inventory at once. There is no other writer of initialFloor, no per-token override, and _floorOf is written only by a completed buySpecial — which can no longer execute once the floor is out of reach. The state is terminal for every affected badge and no actor, including the admin, can undo it.

The monotonicity check exists to stop an admin under-pricing never-traded badges, and its rationale is recorded at L186-L194. That is a recoverable mistake: the badge sells once and the ratchet resumes. The check converts the opposite mistake into an unrecoverable one, and nothing bounds it.

The trigger is an ordinary decimals slip on a 6-decimal token, not malice.

Vulnerable Scenario:

  1. initialFloor is the deployed 500_000 (0.50 USDC). A Special is claimed and has not traded.
  2. The admin intends 0.50 USDC and passes an 18-decimal figure: setInitialFloor(500_000_000_000_000_000). It is larger than the current value, so L199 accepts it.
  3. currentFloor now returns 5e17 for that badge and every other never-traded Special.
  4. Every buySpecial on them reverts BelowFloor.
  5. setInitialFloor(500_000) reverts InitialFloorBelowCurrent. There is no other path.
03Section · Impact

Impact

Every Special badge that has not yet traded becomes permanently unsaleable, and its holder permanently trapped: buySpecial is their only exit, the order book rejects Special ids, PlakxioBadge forbids wallet-to-wallet transfer, and no burn exists. The platform loses the commission stream on its entire unsold Special inventory. No role can recover the state.

04Section · Recommendation

Recommendation

Add an immutable ceiling, mirroring how flatMinCeiling bounds flatMinFee in the sibling contract. The monotonicity check keeps the edge it was written for; the ceiling closes the one it opened.

diff
+uint256 public immutable initialFloorCeiling;
diff
) QuoteTreasuryManaged(quoteCurrency_, treasury_) {
if (admin == address(0) || address(badge_) == address(0)) revert ZeroAddress();
badge = badge_;
+ initialFloorCeiling = initialFloorCeiling_;
diff
function _setInitialFloor(uint256 newInitialFloor) internal {
if (newInitialFloor == 0) revert ZeroInitialFloor();
+ if (newInitialFloor > initialFloorCeiling) revert AboveCeiling(newInitialFloor, initialFloorCeiling);
uint256 current = initialFloor;
if (newInitialFloor < current) revert InitialFloorBelowCurrent(newInitialFloor, current);

AboveCeiling already exists and carries the right shape. Size initialFloorCeiling against the quote token's decimals at deploy time, the same way flatMinCeiling is.

05Section · Resolution

Resolution

initialFloorCeiling is immutable and bounds the raise-only floor, so a mis-scaled value is refused at the setter rather than permanently stranding the never-traded inventory. Verified at audit-v2.

06Section · Affected files

Affected files

  • src/PlakxioSpecialMarket.sol#L196-L201 and src/PlakxioSpecialMarket.sol#L165-L167 at commit 5c38893
Status
Fixed
F-2026-0005