SelfBuy compares addresses, and the floor ratchets from an unbounded payment, so a holder can price their own badge out of the mechanic permanently
SelfBuy compares addresses rather than economic parties and the floor ratchets from an unbounded payment, so a holder using a second address can price their own badge out of the mechanic for roughly the commission. Accepted risk.
Description
SelfBuy compares addresses, not economic parties:
if (msg.sender == holder) revert SelfBuy(msg.sender);
and newFloor derives from a caller-supplied payment with no bound relative to the current
floor:
uint256 newFloor = (payment * escalated) / BPS_DENOMINATOR;
A holder using a second address they control passes the guard. The badge and almost all of the
payment return to them in the same transaction, so the real cost of driving the floor to any
value F is only the commission: commissionBps / (profitBps + commissionBps) of F, which
is one third at deployed parameters. Working capital is recoverable within the call and so can
be borrowed.
_floorOf is monotone with no reset path anywhere in the contract, so the result is permanent.
Above a floor no rational buyer will meet, the contract's stated property — that every Special
badge is permanently open to purchase at or above its floor — no longer holds for that token,
and the platform's commission stream on it ends.
Documented invariant INV-3 stays green throughout: the floor never decreases. It stops describing a price anyone would pay.
commissionBps has an immutable ceiling but no floor, so at commissionBps = 0 the round trip
costs only gas.
Rated Low because the party who pays is the badge's own holder and the third-party harm is the loss of an option to buy rather than a loss of funds. The same shape is reachable by accident: a buyer passing an 18-decimal figure to a 6-decimal quote token irreversibly prices their own badge out, with no recovery for anyone.
Impact
A holder can drive their own badge's floor to a level no rational buyer will meet, for a cost of roughly one third of the target price, permanently ending the ascending mechanic for that token and the platform's commission stream on it. The party who pays is the badge's own holder, and the third-party harm is the loss of an option to buy rather than a loss of funds. The same terminal state is reachable by accident, through a buyer passing an 18-decimal figure to a six-decimal quote token.
Recommendation
Clamp the value the floor may be derived from, leaving the seller's proceeds untouched so overpaying-as-defence still works up to the cap:
+uint256 public immutable maxRatchetBps; // e.g. 30_000 — one sale may at most triple the floor
-uint256 newFloor = (payment * escalated) / BPS_DENOMINATOR;+uint256 cappedBasis = payment > (floor * maxRatchetBps) / BPS_DENOMINATOR+ ? (floor * maxRatchetBps) / BPS_DENOMINATOR+ : payment;+uint256 newFloor = (cappedBasis * escalated) / BPS_DENOMINATOR;
floor is already loaded at L139. The seller still receives payment - commission in full;
only the stored floor is bounded, so an honest overpay is still rewarded and an unbounded
one-transaction self-lock becomes unreachable.
Resolution
Accepted risk. The escape is priced at roughly 3.8% of the floor being set, and the client's position is that a self-buy is economically identical to any other buyer's while revenue is ecosystem-wide rather than per-badge. That is consistent with the severity assigned here: the party who pays is the badge's own holder. The residual the client identifies — a buyer fat-fingering their own payment — is a client-side input-validation concern, since any on-chain cap would be arbitrary.
Affected files
src/PlakxioSpecialMarket.sol#L134-L149at commit5c38893