Pre-auditThree days of senior audit time for $1,500, credited in full toward your Zealynx audit.3 audit days, credited toward your auditSee how →
M-11·audit-finding

The Special floor registry enforces no monotonicity, so a whitelisted Special-class operator can write the floor downward

Fixed · residualnfterc-1155marketplace
TL;DR

The Special floor registry enforces no monotonicity, so a whitelisted Special-class operator can write the floor downward. Final disposition: Fixed · residual.

Severity
MEDIUM
Method
MManual review
CAT.

Tracker: issue #36 · Re-review ID: RR-01 · Status: Fixed · residual · Verified at: generation-d

Severity: Medium Location: src/PlakxioBadge.sol — setSpecialTerms Introduced by: the floor-registry fix (email finding, 2026-09-16), verified at audit-v3-rereview-handoff

02Section · What's wrong

What's wrong

Moving the Special floor and terms onto the badge was the right fix, and setSpecialTerms validates a lot: caller is a whitelisted settlement contract, activation delay elapsed, class permitted, the id really is Special, completeness, uint128 representability, escalation at or above par, and the commission bounded by the escalation above par.

It does not check that the floor is going up. The write is unconditional:

solidity
specialTerms[tokenId] = SpecialTerms(floor, escalated, commissionBps);

So any whitelisted, active Special-class operator can write a floor lower than the one already stored.

03Section · Why it matters

Why it matters

The ascending ratchet is the guarantee the registry exists to provide — it is what makes "no free escape" true and what the original finding was about. That guarantee is now enforced nowhere at the site where the value lives. PlakxioSpecialMarket happens to satisfy it by construction (newFloor = payment * escalated / BPS), so nothing in the suite can see the gap — the same blind spot described in §5.1 of the re-review handoff: a guard only your own market exercises is a guard no test can see.

The premise is the one you asked us to rate in §10(c)/§5.10, and we rate it the same way you did: a whitelisted settlement contract that is not PlakxioSpecialMarket is in the threat model. This is a live instance of that class — and it is reachable by a correct-looking future market with an ordinary arithmetic bug, not only by a hostile one.

04Section · Recommendation

Recommendation

One comparison, next to the checks already there:

solidity
uint256 current = specialTerms[tokenId].floor;
if (floor < current) revert FloorBelowStored(tokenId, floor, current);

Whether equality should be permitted is your call (a re-write of identical terms is harmless; refusing it is stricter). Then remove the check and confirm a test dies, per your own round-3 practice.

05Section · Remediation

Remediation

Fixed for the stored-floor and holder-proceeds regression. The first write from zero remains reachable and is retained as a documented residual.

setSpecialTerms now refuses a record for an unminted Special, refuses a floor below the stored floor (equality permitted, because an honest sale writes an equal floor when profit and commission are both zero), and refuses a record that would lower the proceeds the stored record promises its holder.

We tested the part of the remediation that was hardest to accept on its face — the claim that the one-base-unit rounding allowance cannot be accumulated. It holds: 300 adversarial writes, including the reset-to-zero-and-repeat sequence, did not erode the holder's promise by more than the single specified base unit, and the floor never fell. The allowance is granted only against a stored record whose commission is zero, in which case the stored promise is the floor, so a record carrying a commission admits no further erosion. Live simulations across two providers reproduced FloorBelowStored(2001000739, 574999, 575000) and the proceeds guard on the deployed badge.

Not closed by this ratchet. A hostile whitelisted Special-class operator can still make the first terms write on a minted, never-traded Special at a one-base-unit floor. The market falls back to its configured initialFloor only while the stored floor is zero, so once that record exists the badge is priced at the dust floor. Our independent test confirmed this privileged first-write path; exposure is bounded by initialFloor, and it requires a hostile or arithmetically incorrect whitelisted operator behind the admin Safe and the activation delay.

On the client's request for a judgement: retain the holder-proceeds rule. It places a small amount of arithmetic on the badge, departing from our earlier guidance to keep arithmetic out of it, and we accept that departure — it is one multiplication and two divisions over values the same function has already validated, and it closes the committed-split problem through the registry's own write path. For a subsequent generation we recommend a lower bound on the first write, or recording the opening price at mint. That change is not implemented in the reviewed code.

06Section · Verification provenance

Verification provenance

Final disposition: Fixed · residual. Verified at generation-d, commit bcf18bb2926589bb8538fa8c322eabc871ff2801. This finding was assessed in the generation-d mitigation review. The report-wide scope and evidence limitations remain applicable; see the final report overview.

Status
Fixed · residual
M-11