Aurellion Labs' $456K Loss and How Olympix Would Have Prevented It
On March 17, 2026, the owner of Aurellion Labs' diamond proxy on Arbitrum sent a routine diamondCut. It registered a new SafeOwnable facet with five functions, initialize(address), owner(), transferOwnership(address), acceptOwnership() and renounceOwnership(), and it passed an empty data field, so nothing ran when the facet went live. The facet's initialize function now sat on the diamond, callable by anyone, with its one-time guard still reading zero.
It stayed that way for eight weeks. At 10:04:11 UTC on May 12, a single transaction called initialize with a contract the attacker controlled, became owner of the diamond, installed a new facet of its own, and used the approvals users had already granted the proxy to pull their USDC. By the end of the block, 456,442.54 USDC had left the diamond for the attacker's wallet.
An Upgrade That Stopped Halfway
A diamond proxy is one contract address that routes each function selector to a separate implementation contract called a facet. The owner adds, replaces or removes facets with diamondCut, which takes the list of selector changes plus an optional initializer address and calldata. When those last two are filled in, the diamond calls the initializer inside the same transaction as the cut, so a new facet's storage is set before anyone else can touch it.
According to Verichains' analysis, the March 17 cut added the SafeOwnable facet with the initializer target set to address(0) and no calldata, and no initializer was invoked as part of the upgrade. The facet relies on OpenZeppelin's Initializable pattern, which keeps a version counter in a storage slot and lets initialize run only while that counter is zero. Because the cut never called it, the counter stayed at zero inside the diamond's storage, and initialize(address) stayed open to the first caller who found it.
The diamond already had an owner, 0x1866...7698, recorded in the diamond's own ownership storage. The new facet's initialize function didn't check that. SlowMist, in an alert summarized by ChainCatcher, put it in the same terms: the diamond had set its owner without going through the initialize path, so the initialized slot was never updated and the function could be run again to overwrite owner permissions.
Eight Weeks Later, One Transaction
The attack transaction was a contract deployment from the attacker's wallet 0x9F49...d5Ca, and everything happened inside it. The newly created contract at 0x4D77...4d7e called initialize(address) on the diamond and named itself as the owner. Verichains reports the OwnershipTransferred event moving control from the legitimate owner to that contract, followed by an Initialized event setting the version counter to 1.
As owner, the contract then called diamondCut itself and added a malicious facet at 0x3CA7...E70f. That facet exposed a pullERC20 function, which ran transferFrom against USDC allowances that users had previously granted to the diamond. Three wallets had live approvals. The largest lost 450,999.72 USDC, a second lost 3.00 USDC and a third lost 1.28 USDC, for 451,004.00 USDC in total.
The diamond also held 5,438.53 USDC of its own. The attacker's contract swept that along with the pulled funds, received 456,442.54 USDC from the diamond, and forwarded the full amount to the attacker's wallet in the same transaction. Eight minutes later, at 10:12:33 UTC, that wallet sent all 456,442.54 USDC out in one call to a contract Blockscout labels Permit2Proxy.
As of November 6, 2026, Verichains' write-up didn't describe a response, statement or remediation from Aurellion Labs, and no team statement turned up in the coverage reviewed for this post.
Ownership That Lived in Two Places
The diamond's owner and the facet's idea of whether it had been set up were stored separately, and nothing tied them together. The diamond's ownership slot said the contract belonged to 0x1866...7698. The facet's initializer slot said nothing had been initialized yet. Each piece of code trusted only its own slot, so initialize(address) saw a fresh contract and handed ownership to whoever called first.
That gap turned every approval pointed at the diamond into exposure. Users who had approved the proxy to spend their USDC were trusting whoever controlled the diamond's facets, and from March 17 onward that control was one unauthenticated call away. The attacker didn't need to break the USDC contract, the approvals or the diamond's routing. Ownership came first, a new facet second, and the allowances did the rest.
Verichains' recommended fix is short. The initialize call should be encoded into the diamondCut's own initializer fields, so the facet is set up atomically with the cut, or the facet's constructor should call _disableInitializers(). Verichains specifically warns against leaving initialization to a later transaction, which is exactly the window this upgrade opened and never closed.
The class is a familiar one in upgradeable systems. An open setup routine cost Enjin $176K in August, when two registries behind its legacy CryptoItems contract let anyone re-run setup and become manager.
How Olympix Would Have Closed the Window Before March 17
Olympix models a contract as states, transitions and entry points, and a diamond is a particularly good fit for that view because every facet a cut adds becomes a new set of entry points on the same storage. The state that matters most here is who owns the diamond, since ownership controls diamondCut and diamondCut controls every function the proxy can run. Any entry point that can write that state is a path worth mapping.
Static analysis would have surfaced the signal on the facet itself: a public initialize(address) that writes the owner, guarded only by an initializer flag, being added through a diamondCut whose initializer target is address(0) and whose calldata is empty. That pairing leaves the guard at its default and the setter reachable by any address, and it's visible in the upgrade code a team writes before it ever sends the cut.
From there, BugPoCer would have run Counterexample-Guided Invariant Refutation from the diamond outward. The invariant is plain in the code around it: once the diamond has an owner, only that owner can change who owns it, and only the owner can add facets. BugPoCer would have searched for a sequence of calls that breaks that rule, and the shortest one is a single call to initialize(address) from an address the diamond has never seen. Extending the sequence, the new owner calls diamondCut and gains the ability to spend any allowance granted to the proxy.
The output would have been a counterexample with the minimal conditions spelled out, a facet added without initialization and any address calling initialize, plus a generated Forge test that replays the takeover against a fork of the diamond. That would have been the May 12 drain, produced in development before the March upgrade was ever sent. Because Olympix runs at every merge and before release, the check sits on exactly the kind of change that opened this window, a facet addition that looks too small to need a second look.
What Builders Can Take From Aurellion
Upgrades are code, and the moment a diamondCut lands, every selector it adds is as live as the original contract. A facet with its own initializer needs that initializer executed inside the cut, or disabled in its constructor, and the diamond's existing owner state should be the thing any ownership facet reads, so there's only one answer to who's in charge. Teams that keep proxy approvals from users should treat any change to ownership logic as a change to custody of those funds. To see how Olympix maps the ownership paths in your own upgradeable contracts and returns a runnable counterexample for each one that breaks, book a technical walkthrough.
On-Chain References
Chain: Arbitrum One
Date: May 12, 2026, 10:04:11 UTC
Loss: 456,442.54 USDC
Victim diamond proxy: 0x0adc...b1f2
Legitimate owner before takeover: 0x1866...7698
Attacker EOA: 0x9F49...d5Ca
Attacker contract (new owner): 0x4D77...4d7e
Malicious facet: 0x3CA7...E70f
Uninitialized facet upgrade tx (March 17, 2026): 0x1868...86d1
Exploit tx: 0x19cb...fe0a
Outbound transfer tx: 0x7c32...33c0
More post-mortems
See every post-mortem →See what Olympix finds on your own code
Send us a repo that's already been audited, and we'll run Olympix at the same commit and show you what comes back.




