Audius Governance Storage-Collision Re-initialization Takeover (2022-07-23)

  • Name: Audius governance storage-collision re-initialization takeover
  • Date: 2022-07-23
  • Protocol: Audius
  • Chain: Ethereum
  • Category: unprotected initializer / re-initialization ownership takeover / proxy-storage collision / governance takeover
  • Loss: 18,564,497 AUDIO transferred from the community treasury (about $6 million at the time of transfer; the attacker realized roughly 704 ETH after dumping)
  • Summary: Audius ran governance, staking, and delegation contracts behind a custom AudiusAdminUpgradeabilityProxy that stored proxyAdmin in storage slot 0. The proxied implementations inherited OpenZeppelin’s older Initializable, which also expected its _initialized and _initializing booleans in slot 0. Because the governance contract address stored as proxyAdmin ended with bytes 0xabac, both booleans decoded as truthy inside the implementation context. That made the initializer modifier always succeed and, fatally, treat calls as non-top-level initialization so the flags never changed. The attacker repeatedly called initialize() on Governance, Staking, and DelegateManagerV2, rewired privileged contract pointers to an attacker-controlled governance contract, fabricated two enormous internal delegations of 10,000,000,000,000 AUDIO, then used that fake voting power to pass proposal #85 and transfer 18,564,497 AUDIO out of the community treasury.
  • Trigger / entry point:
    • The critical entry point was repeated initialize() access on proxied Audius contracts that were supposed to be one-time initialized already.
    • Audius’s custom proxy stored proxyAdmin in slot 0 instead of using an isolated EIP-1967-style admin slot.
    • The proxied implementations inherited an Initializable layout that also used slot 0 for the two initialization booleans.
  • Root cause:
    • This was not a simple missing-initializer bug. The guard existed, but its state lived in a storage domain that collided with proxy metadata.
    • In Audius’s deployment, slot 0 of the proxy already contained the governance contract address 0x4DEcA517D6817B6510798b7328F2314d3003AbAC as proxyAdmin.
    • Under delegatecall, the implementation read that same slot as _initialized and _initializing. The low bytes 0xac and 0xab made both booleans non-zero.
    • Because _initializing appeared true, the initializer modifier treated calls as nested initialization rather than a top-level one, so it never reset or finalized the flags. Repeated initialize() calls therefore kept succeeding.
    • Once the attacker could re-run initialization on Governance, Staking, and DelegateManagerV2, they could rewrite governance addresses, guardian / voting configuration, and the internal delegation state that determined proposal power.
  • Exploit path:
    1. The attacker recognized that Audius proxy slot 0 collided with Initializable slot 0, making initialize() replayable on already-deployed contracts.
    2. In transaction 0x3bbb15f9852c389e8d77399fe88b49b042d0f22aad4a33c979fbabc60a34b24f, they called initialize() across the affected contracts and made a first attempt to fabricate internal delegation state large enough to pass a malicious proposal. That attempt failed because the proposal did not receive votes.
    3. In transaction 0xfefd829e246002a8fd061eede7501bccb6e244a9aacea0ebceaecef5d877a984, they repeated the sequence, again reinitializing Governance, Staking, and DelegateManagerV2.
    4. During those replayed initialization calls, the attacker changed the Governance contract’s guardian settings and pointed the governance addresses inside Staking and DelegateManagerV2 at an attacker-controlled governance contract 0xbdbb5945f252bc3466a319cdcc3ee8056bf2e569.
    5. Using the corrupted staking / delegation state, the attacker recorded two bogus delegations of 10,000,000,000,000 AUDIO to themselves. No real tokens were minted, but Audius governance logic treated the altered storage as legitimate voting power.
    6. With that fabricated quorum, the attacker pushed proposal #85, which instructed the community treasury to transfer its AUDIO balance to the attacker-controlled address.
    7. In transaction 0x4227bca8ed4b8915c7eec0e14ad3748a88c4371d4176e716e8007249b9980dc9, the treasury transfer executed and moved 18,564,497 AUDIO to the attacker.
    8. The attacker then dumped the stolen AUDIO in a single Uniswap transaction for about 704 ETH, turning a governance-storage exploit into realized theft.
  • What the attacker needed to be true:
    • The proxy had to store admin metadata in an implementation-colliding storage slot rather than an isolated admin slot.
    • The implementations had to rely on Initializable state stored in that same colliding slot.
    • Governance, Staking, and DelegateManagerV2 each had to expose replayable initialize() paths that wrote privileged configuration.
    • Audius governance had to accept staking / delegation storage as the source of voting truth, even though the apparent balances were only written into contract state rather than backed by real tokens.
    • The treasury-transfer action had to be reachable through governance alone once quorum and proposal state were forged.
  • Technical anatomy worth remembering:
    • Audius is a useful case because the initializer guard was present. The failure sat one layer lower, in proxy / implementation storage layout.
    • The memorable detail is the trailing 0xabac bytes of the governance address stored in slot 0: those bytes made _initializing and _initialized look permanently truthy.
    • The exploit turned a storage-layout bug into a control-plane seizure rather than a direct vault-drain primitive. The attacker first stole governance, then used governance to move treasury funds.
    • The fake 10T AUDIO delegations were internal accounting artifacts, not minted supply. The exploit path depended on governance trusting those mutated storage values as valid vote weight.
    • This incident is also a reminder that custom proxy tweaks around admin storage are security-critical. Deviating from EIP-1967-style reserved slots can quietly invalidate otherwise standard implementation assumptions.

Three governance-authority promotions this incident collapsed

  1. Some slot-0 bytes already existed and some setup selectors were still reachable

    • The proxy already held a live proxyAdmin address in slot 0, and the proxied Governance / Staking / DelegateManagerV2 contracts still exposed initialize() entry points.
    • That was only the weakest fact: the shared storage word contains bytes and constructor-like code can still be called.
  2. Those bytes were over-promoted into canonical initialization truth

    • Under delegatecall, Audius treated the same slot-0 word as OpenZeppelin Initializable state.
    • Because the governance address ended in 0xabac, _initializing and _initialized both decoded as truthy, so repeated initialize() calls kept looking like legitimate nested initialization rather than forbidden replay.
    • That mistaken freshness story let the attacker rewrite governance pointers, guardian state, and delegation accounting without ever needing real token movement.
  3. That counterfeit governance state still inherited full treasury authority

    • Once the attacker-authored staking and delegation state was accepted as real vote weight, proposal #85 was treated as ordinary community-governance output.
    • The treasury did not distinguish between delegation records exist, those records are economically grounded vote truth, and that accepted vote truth may now move 18.5M AUDIO in one step.
    • The final transfer tx was therefore not a separate auth failure; it was the treasury faithfully honoring already-counterfeit governance truth.
  • Representative transactions and addresses:
    • First malicious initialization / failed attempt tx: 0x3bbb15f9852c389e8d77399fe88b49b042d0f22aad4a33c979fbabc60a34b24f
    • Second malicious initialization / forged-vote tx: 0xfefd829e246002a8fd061eede7501bccb6e244a9aacea0ebceaecef5d877a984
    • Treasury-drain tx: 0x4227bca8ed4b8915c7eec0e14ad3748a88c4371d4176e716e8007249b9980dc9
    • Attacker-controlled governance contract: 0xbdbb5945f252bc3466a319cdcc3ee8056bf2e569
    • Governance / proxyAdmin address whose slot-0 bytes caused the collision: 0x4DEcA517D6817B6510798b7328F2314d3003AbAC
    • Attacker EOA noted in public writeups: 0xa0c7BD318D69424603CBf91e9969870F21B8ab4c
  • Response / mitigation:
    • Audius and incident responders, including samczsun, determined the exploit was still live and raced to regain control using the same initialization weakness before a follow-on drain could happen.
    • The team temporarily upgraded affected contracts to a blocking implementation that froze writes and, as a side effect, briefly halted token transfers while control was re-established.
    • Audius then patched its initialization layout by adding padding so the implementation’s initialization state no longer overlapped proxy admin storage, and restricted initialization so only the proxy admin could perform the final recovery initialization.
    • The team later restored the affected contracts and published a detailed postmortem plus recovery repo documenting the takeover fix.

Pattern tags

  • storage-layout-collision
  • unprotected-initializer-reinitialization
  • Proxy metadata in slot 0 aliased OpenZeppelin Initializable state, so a standard-looking one-time guard authenticated the wrong storage domain
  • Replayed initialize() calls rewired governance pointers and delegation state rather than directly stealing treasury funds on the first call
  • The treasury drain was a second-stage monetization step after governance truth had already been corrupted

Why the exploit worked

  • Audius’s proxy and implementation disagreed about what slot 0 meant. The proxy thought it stored proxyAdmin; the implementation thought it held _initialized and _initializing. Once those two stories shared the same storage word under delegatecall, the initializer guard stopped being trustworthy.
  • The trailing 0xabac bytes of the live proxyAdmin address made both initialization booleans decode as truthy. That pushed the initializer modifier down its nested-initialization branch on every call, so the contracts never reached the path that would permanently close setup.
  • Replayable initialization did not need to mint AUDIO directly. It only needed to rewrite the governance, staking, and delegation pointers that Audius used as vote-power truth. The fake 10T AUDIO delegations were internal accounting artifacts, but governance still treated them as enough quorum to authorize a real treasury transfer.
  • This means the exploit surface and the root cause sit on different layers: the visible primitive was initialize() replay, but the decisive bug was storage-authentication failure inside the proxy architecture. The later treasury transfer merely honored governance state that was already counterfeit.

Detection and audit pivots

  • Diff proxy storage and implementation storage together, not separately. If a custom proxy keeps admin metadata outside EIP-1967-style reserved slots, ask which implementation-side guards, booleans, or packed flags now read those same bytes.
  • Re-run initialization on shadow forks and already-live deployments during review. A contract that emits or accepts a meaningful initialize() path after launch should be treated as an authorization incident until proven otherwise.
  • Treat initializer present as an incomplete audit answer. The sharper question is what exact live slot makes this modifier think setup already happened, and who else writes that slot?
  • Follow governance vote power back to its real source of truth. If staking or delegation storage can be rewritten through replayed setup, governance capture is the expected downstream symptom even when token balances and total supply never changed.
  • Test whether governance-controlled treasury actions remain safe after internal vote-weight corruption. Audius is a reminder that balance-sheet loss may arrive one step after the original bug, through an otherwise ordinary proposal-execution path.

Failure-prevention checklist

  • Keep proxy metadata and implementation auth state in disjoint namespaces. If a contract uses upgradeable proxies, admin slots, implementation slots, initializer flags, and ownership state should all live in layouts that cannot alias under delegatecall; in practice that means standard reserved slots or an equally explicit equivalent.
  • Treat initializer guards as part of authorization, not setup ergonomics. A protocol should prove on live deployed instances that initialize() cannot succeed again, including after proxy upgrades, storage-layout edits, or emergency hotfixes.
  • Make replayed initialization fail closed before any downstream governance or treasury path can matter. If governance, staking, or delegation pointers are writable during setup, then setup is already treasury authority.
  • Bind governance vote power to a storage story that cannot be rewritten by configuration-only flows. Internal delegation or staking state should not become treasury-moving quorum if the same contracts still expose replayable bootstrap paths.
  • Exercise recovery and emergency-upgrade tooling against the same storage assumptions the production fleet uses. Audius shows that custom proxy deviations are not harmless implementation details; they are part of the live auth boundary.
  • Monitor for impossible post-launch setup activity: repeated initialize() calls, governance-address changes inside staking modules, or sudden vote-weight jumps unsupported by real token movement.
  • Prefer bounded-loss treasury design even after governance auth is compromised. Proposal execution that can move the full treasury in one step turns a configuration bug into immediate balance-sheet loss.

Audit prompts

  • Which exact storage word tells this deployed proxy-backed system that initialization is already done, and who else can write or reinterpret that word?
  • If a custom proxy deviates from EIP-1967 or another reserved-slot scheme, what proof shows that no implementation-side guard, boolean packing, or inherited base contract reads those same bytes differently?
  • Can initialize() or any setup-equivalent selector still mutate governance pointers, guardian roles, vote accounting, or staking/delegation relationships on a live funded deployment?
  • Does the protocol’s notion of governance power come from balances and checkpoints that are economically anchored, or from internal delegation state that can be rewritten through setup or config paths?
  • If governance were already counterfeit, what concrete control would stop proposal execution from immediately emptying the treasury: caps, delay, veto, staged release, or nothing?
  • Have shadow-fork tests actually replayed initialization on production addresses and verified that repeated calls fail for the right storage reason rather than by accident?
  • Which emergency upgrade, migration, or recovery flows could make a live proxy appear fresh again by changing layout, inheritance order, or initializer state assumptions?
  • What telemetry would tell operators that a storage-collision bug had already become governance capture before the treasury transfer landed?

Lessons that should already be standard

  • A present initializer modifier is not meaningful by itself. The live security question is whether the modifier authenticates the right storage domain on the real deployed proxy.
  • Custom proxy layout is governance and treasury security, not just upgradeability plumbing. One misplaced admin slot can silently redefine who still has setup authority over the whole protocol.
  • Governance capture can begin as a storage bug long before it looks like a voting bug. Once internal delegation truth is attacker-authored, proposal execution is only the cash-out step.
  • Internal vote weight that is not tightly tied to economically anchored balances should be treated as a dangerous derived surface, especially when bootstrap or config paths still write it.
  • Incident response that must exploit the same bug to regain control is evidence that the protocol left too little graceful containment between setup auth failed and treasury can move now.
  • Full-treasury proposal execution is an avoidable amplifier. Even if governance is compromised, bounded release and staged treasury controls can keep one auth failure from becoming total loss in one transaction.

Why this incident matters

  • Audius is a sharp example of how a protocol can have a correct-looking initializer guard and still be completely broken because proxy metadata collides with implementation state.
  • It shows how storage-layout errors frequently surface first as governance or admin compromise, with the actual asset movement happening only after control has already been lost.
  • It also preserves a useful accounting lesson: governance systems may trust internal delegation state long before anyone notices that the apparent quorum was never backed by real token ownership.
  • In corpus terms, it broadens the initializer-takeover family beyond naked init() bugs like Parity into upgradeable-proxy guard bypasses where the one-time flag exists but cannot be trusted.

Internal linkages

Control surface