Meta — Multi-Owner Stacking
TL;DR: MultiOwnable is flat: any owner has full
onlyOwnerspower, no majority, no veto. Adding someone = full trust. Design around that, don’t work around it.
Why this matters
Section titled “Why this matters”Dysnomia’s access control is MultiOwnable, not OpenZeppelin’s single-owner Ownable. Every QING, LAU, WORLD, YUE, and most game contracts inherit it. That’s a deliberate architectural choice — many contracts need cross-invocation rights, which require multi-ownership — but it has sharp consequences:
- Adding an owner is a total-trust operation. The new owner can use every owner-gated function, including
Withdraw,Rename,AllowCROWS, market-rate changes andrenounceOwnership(you). Bouncer controls remain a separate role. - There’s no M-of-N. MultiOwnable is 1-of-anybody. A single rogue owner can do damage instantly.
- There’s no on-chain quorum. If you want multisig semantics you have to wrap the contract in an external multisig (Gnosis Safe, etc.) and make the Safe the sole owner.
Many “shared venue” ambitions go bad because teams treat MultiOwnable like a co-signer list. It isn’t.
The numbers
Section titled “The numbers”What MultiOwnable actually provides
Section titled “What MultiOwnable actually provides”From MultiOwnable:
| Function | Signature | Effect |
|---|---|---|
addOwner(newOwner) |
public onlyOwners |
Any existing owner can add any address. No cooldown, no approval. |
renounceOwnership(toRemove) |
public onlyOwners |
Any existing owner can remove any address (including other owners). |
owner() |
external view | Returns the contract’s own address. It is not an owner-list accessor. |
Consequences at the game level
Section titled “Consequences at the game level”- QING co-ownership: direct owners control
AllowCROWS, market rates, withdrawals and ownership changes. Cover, staff and guest-list controls require bouncer authority, which ownership alone does not provide. See QING. - LAU co-ownership: any owner controls the LAU’s Purchase/Redeem rate seeding, Chat routing, and balance. See LAU.
- YUE routing:
Withdrawis bothonlyOwnersand restricted to calls from CHAN. The player’sOriginis a separate routing field, not a MultiOwnable role. See YUE. - Contract-contract ownership is common. YANG addOwners itself to YAU, ZHOU, ZHENG, YI during construction (see YANG constructor). The protocol’s contracts co-own each other as part of normal operation.
What you cannot do natively
Section titled “What you cannot do natively”- Require 2-of-3 approvals.
- Put an owner on “view-only.”
- Schedule time-locked adds/removes.
- Detect owner activity per-address (only
owner()is exposed).
For any of those, you need an external multisig fronting your ownership slot.
The play
Section titled “The play”- Default to solo ownership. Unless you need someone else to have equal control, don’t add them. The marginal convenience of shared write-access is almost never worth the blast radius.
- If you must co-own, use a Safe. Deploy a Gnosis Safe (or equivalent),
addOwner(safeAddress), thenrenounceOwnership(yourEOA). Now the only on-chain owner is the Safe, which enforces M-of-N off-chain semantics via its signature threshold. - Never co-own with an unverified contract. Any contract that’s an owner can be upgraded or exploited into draining your QING. If you add a contract as owner, read its source.
- For QING venues, prefer the staff/guestlist layer.
setStaff(addr, true)gives bouncer-ish rights without full ownership. You can always revoke. Use staff for operational roles; reserve ownership for treasurers. - Treat
renounceOwnership(yourself)as a nuclear option. Once you’re out, you cannot re-add yourself; only another owner can, and if you’ve just evicted yourself from a bad-faith co-owner, they won’t. Test on a throwaway QING first. owner()is not an audit tool. It returns the contract address. To enumerate owners, read theOwnershipUpdateevent log and useowner(address)for candidate checks.- (inferred) Protocol contracts as owners are semi-permanent. YANG’s construction adds itself to YAU/ZHOU/ZHENG/YI. Don’t try to “clean up” those relationships — they’re load-bearing.
Use these calls here. Simulations preview the current state; each confirmed call is a separate transaction.
QING.owner(address cOwner)Read
Open this function to load its call form.
QING.addOwner(address newOwner)Simulate / call
Open this function to load its call form.
QING.renounceOwnership(address toRemove)Simulate / call
Open this function to load its call form.
QING.setStaff(address _a, bool active)Simulate / call
Open this function to load its call form.
Worked example
Section titled “Worked example”You’re a venue operator deploying a new QING with two partners (Alice, Bob). You want: any 2 of 3 must approve withdrawals; all 3 can moderate; you alone can set cover.
Naïve approach (do NOT do this):
addOwner(Alice)addOwner(Bob)Result: all three have full power. Alice can Withdraw unilaterally. Bob can renounceOwnership(you) and run off with the treasury.
Correct approach:
- Deploy a Gnosis Safe with Alice, Bob, you as signers, threshold 2-of-3.
QING.addOwner(safe).- Decide which operational rights are “ops” (everyday) vs “treasury” (high-value):
- For ops:
setStaff(you, true); setStaff(Alice, true); setStaff(Bob, true)— bouncer-style rights without ownership. - For treasury: only the Safe is an owner; withdrawals require 2-of-3.
- For ops:
renounceOwnership(yourEOA)(and Alice’s and Bob’s EOAs if any were ever added).
Now cover-setting (setCoverCharge) remains available to staff through onlyBouncers, while Withdraw requires the Safe’s 2-of-3.
Result: operational agility + treasury safety, without MultiOwnable being the trust surface.
Gotchas
Section titled “Gotchas”renounceOwnership(toRemove)accepts arbitrary addresses. There is no “remove self” safety — an owner can remove any owner, including the original deployer.addOwnerhas no cooldown. Rotating the ownership set is instant. An attacker with a private key briefly in their hands can rearrange ownership permanently.- No event filter by “current owners.” The
OwnershipUpdateevent emits(newOwner, state); to get the current set you replay all events. - CROWS + MultiOwnable interaction at QINGs. If
NoCROWS == false, CROWS holders ≥ 25 act as bouncers — but bouncers aren’t owners. They can’t add/remove owners. This is a sharp distinction; see QING. - Owner ≠ bouncer. Being an owner doesn’t automatically make you a bouncer. Bouncer status is separately derived from CROWS/Staff/Divisor-share logic.
- Protocol owners cascade. If YANG is an owner of YAU (which it is per YANG), then whoever owns YANG’s contract indirectly owns YAU. Upgrade paths matter.
- (inferred)
onlyOwner≠onlyOwners. DSS uses singularonlyOwner(one owner); most Dysnomia contracts use pluralonlyOwners(MultiOwnable). Mixing them in a custom extension silently changes semantics. Read the modifier signature, not just the name. - renounceOwnership doesn’t burn; it transfers-out. There’s no “seal the contract forever” path unless you renounce every owner. Once the last owner renounces, all
onlyOwnerspaths revert — the contract becomes immutable in admin-space but continues to serve its public interface.
Where it cross-connects
Section titled “Where it cross-connects”- Venues: Cover-Charge Tuning — operator-side controls that MultiOwnable enables.
- Venues: Guest-List Rotation —
setStaff+setGuestlistas lower-trust alternatives. - Meta: DSS Bundling — the lone
onlyOwner(singular) outlier. - Meta: Word Games Tactics — ACRONYM round advancement often needs an owner to trigger.
- Timing: First-Mover Tradeoffs — early deployers of QINGs set ownership structure before trust is established.
- Cheat Sheet: Constants — no numeric constants for MultiOwnable, but referenced in the architecture overview.