The Special floor registry enforces no monotonicity, so a whitelisted Special-class operator can write the floor downward
The Special floor registry enforces no monotonicity, so a whitelisted Special-class operator can write the floor downward. Final disposition: Fixed · residual.
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
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:
specialTerms[tokenId] = SpecialTerms(floor, escalated, commissionBps);
So any whitelisted, active Special-class operator can write a floor lower than the one already stored.
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.
Recommendation
One comparison, next to the checks already there:
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.
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.
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.