Skip to content
Alpha & Strategy

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.

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 and renounceOwnership(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.

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.
  • 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: Withdraw is both onlyOwners and restricted to calls from CHAN. The player’s Origin is 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.
  • 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.

  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 the contract address. To enumerate owners, read the OwnershipUpdate event log and use owner(address) for candidate checks.
  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.

Use these calls here. Simulations preview the current state; each confirmed call is a separate transaction.

QINGResolve your player or venue above the call forms.
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.

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):
    • 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.
  4. 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.

  • 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.