setInitialFloor has no upper bound and no inverse, so one mis-scaled call permanently freezes the never-traded Special inventory
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.
Description
_setInitialFloor rejects zero and rejects any value below the current one, and imposes no
upper bound:
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:
initialFlooris the deployed500_000(0.50 USDC). A Special is claimed and has not traded.- 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. currentFloornow returns 5e17 for that badge and every other never-traded Special.- Every
buySpecialon them revertsBelowFloor. setInitialFloor(500_000)revertsInitialFloorBelowCurrent. There is no other path.
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.
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.
+uint256 public immutable initialFloorCeiling;
) QuoteTreasuryManaged(quoteCurrency_, treasury_) {if (admin == address(0) || address(badge_) == address(0)) revert ZeroAddress();badge = badge_;+ initialFloorCeiling = initialFloorCeiling_;
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.
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.
Affected files
src/PlakxioSpecialMarket.sol#L196-L201andsrc/PlakxioSpecialMarket.sol#L165-L167at commit5c38893