Skip to content

Meta — Multi-Owner Stacking

TL;DR: MultiOwnable is flat: any owner has full onlyOwners power, no majority, no veto. Adding someone = full trust. Design around that, don't work around it.

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 Withdraw, Rename, setCoverCharge, setGuestlist, removeGuest, and even renounceOwnership(you) to evict you.
  • 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

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 an owner (a representative). Not the full list.

Consequences at the game level

  • QING co-ownership: any owner can setCoverCharge, setStaff, setNoCROWS, setBouncerDivisor, setGuestlist, removeGuest, Withdraw. See QING.
  • LAU co-ownership: any owner controls the LAU's Purchase/Redeem rate seeding, Chat routing, and balance. See LAU.
  • YUE co-ownership: any owner can Withdraw YUE-held balances. 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

  • 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

  1. 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.
  2. If you must co-own, use a Safe. Deploy a Gnosis Safe (or equivalent), addOwner(safeAddress), then renounceOwnership(yourEOA). Now the only on-chain owner is the Safe, which enforces M-of-N off-chain semantics via its signature threshold.
  3. 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.
  4. 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.
  5. 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.
  6. owner() is not an audit tool. It returns an owner, not the full set. To enumerate owners, read the OwnershipUpdate event log. Build a dashboard; don't rely on one-shot view calls.
  7. (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.

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:

  1. Deploy a Gnosis Safe with Alice, Bob, you as signers, threshold 2-of-3.
  2. QING.addOwner(safe).
  3. Decide which operational rights are "ops" (everyday) vs "treasury" (high-value):
  4. For ops: setStaff(you, true); setStaff(Alice, true); setStaff(Bob, true) — bouncer-style rights without ownership.
  5. For treasury: only the Safe is an owner; withdrawals require 2-of-3.
  6. renounceOwnership(yourEOA) (and Alice's and Bob's EOAs if any were ever added).

Now cover-setting (setCoverCharge) is still onlyBouncers; staff can do it. But Withdraw requires the Safe's 2-of-3.

Result: operational agility + treasury safety, without MultiOwnable being the trust surface.

Gotchas

  • renounceOwnership(toRemove) accepts arbitrary addresses. There is no "remove self" safety — an owner can remove any owner, including the original deployer.
  • addOwner has 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 OwnershipUpdate event 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) onlyOwneronlyOwners. DSS uses singular onlyOwner (one owner); most Dysnomia contracts use plural onlyOwners (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 onlyOwners paths revert — the contract becomes immutable in admin-space but continues to serve its public interface.

Where it cross-connects