Admin renunciation is a single irreversible call, and the walled garden turns admin loss into permanent illiquidity for every holder
Admin renunciation is a single irreversible call, and because badges can only move through whitelisted markets, losing the admin would leave every holder permanently illiquid.
Description
All three contracts inherit OpenZeppelin AccessControl unmodified — PlakxioBadge
directly, and PlakxioSettlement and PlakxioSpecialMarket through their shared
QuoteTreasuryManaged base. DEFAULT_ADMIN_ROLE is
its own admin, grantRole accepts any address with no acceptance step, and renounceRole
executes in one call with only a self-confirmation:
function renounceRole(bytes32 role, address callerConfirmation) public virtual {if (callerConfirmation != _msgSender()) {revert AccessControlBadConfirmation();}_revokeRole(role, callerConfirmation);}
OpenZeppelin's own AccessControlDefaultAdminRules — two-step and delayed, and recommended in
a comment in the very file being inherited — is available and unused.
The generic form of this is well known. What makes it worth raising here is what the walled
garden does to the consequence. A handover that grants to a mistyped address and then renounces
leaves PlakxioBadge with no reachable admin, so setSettlementContract can never be called
again and the whitelist plus every class mask freeze permanently. Because the badge admits no
wallet-to-wallet transfer and exposes no burn, every badge in every wallet becomes permanently
immobile except through whichever settlement contract happened to be whitelisted at that
moment — and the settlement contract has already been redeployed several times over this
epic, so a replacement could never be whitelisted and a superseded one never revoked.
The same call on the other two contracts freezes feeBps, flatMinFee, the escalation
parameters, initialFloor and treasury. Freezing treasury is the sharper one: it is the
recovery lever for a treasury that can no longer receive the quote token, so losing it turns a
bounded outage into a permanent one.
Impact
A handover that grants to a mistyped address and then renounces leaves the badge with no reachable admin. Because the badge admits no wallet-to-wallet transfer and exposes no burn, every badge in every wallet becomes permanently immobile except through whichever settlement contract happened to be whitelisted at that moment. The same call on the other two contracts freezes the fee parameters, the escalation split, the floor and the treasury address — the last of which is the recovery lever for a treasury that can no longer receive the quote token.
Recommendation
Replace AccessControl with AccessControlDefaultAdminRules on all three contracts:
-import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";+import {AccessControlDefaultAdminRules} from "@openzeppelin/contracts/access/extensions/AccessControlDefaultAdminRules.sol";
-contract PlakxioBadge is ERC1155, AccessControl {+contract PlakxioBadge is ERC1155, AccessControlDefaultAdminRules {
with the constructor taking an initialDelay and the initial admin, replacing the current
_grantRole(DEFAULT_ADMIN_ROLE, admin). This makes the default-admin transfer two-step and
delayed and disables bare renounceRole for that role. supportsInterface already overrides
both parents and needs its override list updated to match.
Resolution
All three contracts now use OpenZeppelin's two-step, delayed default-admin rules, so the handover
cannot complete in a single call and a mistyped grant is recoverable within the window. Verified
at audit-v2.
Affected files
src/PlakxioBadge.sol#L17andsrc/QuoteTreasuryManaged.sol#L27at commit5c38893