DeltaPrime Prime Account DiamondBeacon init() Selector Re-initialization Hijack (2024-07-23)
- Name: DeltaPrime Prime Account DiamondBeacon
init()selector re-initialization hijack - Date: 2024-07-23
- Protocol: DeltaPrime
- Chain: Arbitrum
- Category: unprotected initializer / re-initialization ownership takeover / storage-layout collision / proxy selector exposure
- Loss: about 900k later returned and the remaining ~$100k covered by DeltaPrime
- Summary: DeltaPrime’s leveraged user vaults (
Prime Accounts) used a DiamondBeaconProxy-style architecture where account contracts stored user state while shared implementation contracts were reached throughdelegatecall. A deployment-onlyDiamondBeacon.init()path with selector0xe1c7392awas mistakenly left reachable through live Prime Accounts even though the accounts were meant to expose only their owninitialize()path (0xc4d66de8). The two initializer routines checked different_initializedflags in different custom storage slots, so calling the beacon’sinit()logic from an already-initialized Prime Account bypassed the account’s real one-time-init guard while still writing a newownerinto the Prime Account state slot. After seizing ownership of 13 victim Prime Accounts, the attacker repaid each account’s debt, unwound positions, and withdrew the collateral, draining about290.3 ETHbefore DeltaPrime paused Prime Accounts. - Trigger / entry point:
- DeltaPrime Prime Accounts were proxy-style storage contracts that resolved function selectors through a shared
DiamondBeaconsingleton. - The beacon still exposed a deployment-time
init()method with selector0xe1c7392aafter production launch. - Prime Accounts did not implement that selector themselves, so selector resolution forwarded the call into
DiamondBeaconlogic and executed it inside the Prime Account storage context viadelegatecall. - The beacon initializer checked its own
_initializedflag in beacon storage namespace rather than the Prime Account initializer flag in the account’s general-state namespace.
- DeltaPrime Prime Accounts were proxy-style storage contracts that resolved function selectors through a shared
- Root cause:
- DeltaPrime left a deployment-only initializer mounted on the live shared-implementation surface instead of removing it once setup was complete.
- The beacon-side
init()and account-sideinitialize()routines both tried to enforce one-time initialization, but they read different_initializedbooleans from different custom storage slots. - Because
delegatecallexecuted beacon code against Prime Account storage, the beacon initializer’s guard looked at the wrong slot, sawfalse, and proceeded. - That same call path still wrote the
ownerfield into the Prime Account’s general-state slot, so a function that should only have been usable during system bootstrap became a live ownership-transfer primitive.
- Exploit path:
- The attacker first rehearsed the bug against their own Prime Accounts to confirm that
init()could illegitimately change account ownership without tripping the account’s normal initialization guard. - Starting around
2024-07-23 22:15 UTC+2, the attacker called the strayinit()selector on victim Prime Accounts, causing beacon logic to run in each account’s storage context and overwrite the accountownerwith an attacker-controlled address. - Once owner of a victim Prime Account, the attacker interacted with ordinary Prime Account methods to unwind leveraged positions and repay borrowed funds. DeltaPrime emphasized that the attacker still had to respect the remaining solvency and withdrawal guards; the ownership hijack did not disable those checks.
- After repaying the debt, the attacker withdrew the freed collateral from each hijacked Prime Account.
- Across 13 accounts, the attacker extracted about
290.3 ETHbefore DeltaPrime confirmed the exploit and paused Prime Accounts at2024-07-24 00:10 UTC+2.
- The attacker first rehearsed the bug against their own Prime Accounts to confirm that
- What the attacker needed to be true:
- Prime Accounts had to resolve unknown selectors through a shared beacon / facet map instead of rejecting unrecognized calls outright.
- The production system had to keep the deployment-time
init()selector live on the beacon. - The beacon initializer had to check a different
_initializedslot from the one actually used to prove a Prime Account was already initialized. - The initializer logic also had to retain write access to the live Prime Account
ownerfield. - Victim Prime Accounts had to hold withdrawable collateral that could be released after debt repayment and position unwind.
- Technical anatomy worth remembering:
- This is a useful hybrid of re-initialization and storage-layout / storage-namespace failure. The bug was not merely
initializer left callable; it waswrong initializer left callable through a proxy selector surface, checking the wrong storage namespace, while still mutating the right ownership namespace. - DeltaPrime’s postmortem identified two custom storage slots that matter. Prime Account general state, including
ownerand its own_initializedflag, lived at0x8d5bb42e0ac1496a2c326edc9c00758985246e6c2bb146d6c2f4a0d509e0960a. DiamondBeacon-specific state, including selector mappings and a different_initializedflag, lived at0xc8fcad8db84d3cc18b4c41d551ea0ee66dd599cde068d998e57d5e09332c131c. - The memorable selector pair is
init() -> 0xe1c7392aversusinitialize() -> 0xc4d66de8. The first should have been extinguished after deployment; the second was the only initializer a Prime Account was supposed to honor. - The exploit shows why custom storage slots do not save a proxy architecture by themselves. They only help if every reachable implementation is explicitly scoped to the contracts that are supposed to call it.
- It also shows why
delegatecallsystems need an explicit policy for decommissioning bootstrap selectors. Leaving them reachable turns deployment code into production authority.
- This is a useful hybrid of re-initialization and storage-layout / storage-namespace failure. The bug was not merely
- Representative transactions and addresses:
- First publicly cited victim-drain transaction:
0xc4350cf22cabfd88e4221d3496e84cd28841682d60ef89020622844e5879b99f - DeltaPrime’s onchain negotiation message to the attacker:
0xf09f71303f56d72b107c1d169b9a6fbe2633e4c02bd1c5e77470663fefdd2015 - Prime Account state slot:
0x8d5bb42e0ac1496a2c326edc9c00758985246e6c2bb146d6c2f4a0d509e0960a - DiamondBeacon state slot:
0xc8fcad8db84d3cc18b4c41d551ea0ee66dd599cde068d998e57d5e09332c131c - Stray beacon initializer selector:
0xe1c7392a - Intended Prime Account initializer selector:
0xc4d66de8
- First publicly cited victim-drain transaction:
- Response / mitigation:
- DeltaPrime paused Prime Accounts once the attack was confirmed, while leaving Savings accounts open because the lending side was not directly at risk.
- The team timelocked and executed a fix removing the stray
init()path from the DiamondBeacon, then worked with auditors to search for similar selector / storage-slot collisions. - DeltaPrime said two adjacent vectors were also found and fixed during the emergency review.
- The attacker returned about
$900kafter onchain negotiation; DeltaPrime used its stability pool to make affected users whole for the remaining loss. - The protocol also added a 24-hour withdrawal delay after Prime Account owner changes, planned another full external audit, increased bug-bounty payouts, and said it would move toward whitelisting call sequences.
Pattern tags
- unprotected-initializer-reinitialization
- storage-layout-collision
- deprecation-without-deauthorization
Why the exploit worked
- DeltaPrime let live Prime Accounts inherit more selector reach than their product surface implied. The account contract itself did not expose the dangerous deployment-only
init()path, but selector fallback through the shared DiamondBeacon still made that bootstrap authority callable in production. - The decisive guard failure was not simply
initializer missing. The beacon-sideinit()routine did check_initialized, but it checked the wrong lifecycle object: DiamondBeacon storage state rather than the already-funded Prime Account state whoseownerfield the call was about to overwrite. - Because the call executed through
delegatecall, the same invocation could read freshness from one namespace and write ownership into another. That split let a function that should only have ever configured the beacon during rollout become a live ownership-reset primitive for user vaults. - DeltaPrime’s own safeguards then helped frame the exploit cleanly. The attacker still had to unwind positions, repay debt, and pass ordinary withdrawal logic after takeover, which proves the real exploit primitive was authority capture rather than some separate solvency bypass.
- The pre-attack rehearsal also mattered. DeltaPrime’s postmortem says the attacker first changed ownership of several of their own accounts, generating ownership-change alerts that initially appeared benign. That is worth preserving because selector-surface exploits often arrive as
looks like a valid admin action on the wrong objectbefore the malicious pattern becomes obvious.
Detection and audit pivots
- Build a full selector-reachability map for every proxy-style user account, not just an ABI list for the storage contract. The critical DeltaPrime path was
Prime Account fallback -> DiamondBeacon selector lookup -> deployment-only init logic, and teams that only review the user-facing ABI will miss exactly that class of bug. - Separate three questions during review:
can this initializer still be reached,which storage namespace proves it is already spent, andwhich privileged fields can it still mutate under delegatecall. DeltaPrime only fails once all three are composed. - Treat deployment-only or migration-only selectors as live admin surfaces until they are provably extinguished.
Nobody is supposed to call this anymoreis not a control if production selector routing can still reach it. - Monitor for rehearsal-shaped anomalies, not only confirmed drains. Repeated owner changes on attacker-controlled accounts, especially through rare selectors or unusual call traces, can be the dress rehearsal for a broader takeover campaign rather than harmless user behavior.
- After any owner change on a leveraged account, ask what immediate value-extraction sequence becomes possible: debt repayment, position unwind, collateral withdrawal, module installation, or cross-account approvals. DeltaPrime’s added withdrawal delay is a strong signal that ownership changes themselves deserve post-event quarantine.
Why this incident matters
- DeltaPrime is a sharp reminder that proxy / diamond incidents do not need a classic storage-slot collision between old and new implementations to become dangerous. It is enough for one reachable bootstrap function to authenticate against the wrong storage namespace while still mutating live authority.
- It is also a strong example of why
one-time initializeris not a property of a function name. In modular proxy systems, the real question iswhich initializer, checked against which slot, reachable through which selector table, in which storage context? - The healthy boundary that failed here was not abstract. Production Prime Accounts needed the beacon’s deployment-time
init()selector to be extinguished or directly deauthorized once rollout was over, rather than left reachable through live selector routing. - The case is especially worth preserving because the attacker still had to use normal account flows after takeover. The exploit primitive was not arbitrary withdrawal; it was ownership capture of user-controlled leveraged accounts, which the protocol’s own withdrawal logic then honored.
Failure-prevention checklist
- Extinguish deployment-only selectors once rollout is complete. If a shared beacon, facet registry, or singleton still exposes
init()-style setup verbs, make sure live user accounts cannot route to them through selector fallback or facet lookup. - Keep one-time guards in the same storage namespace as the authority they protect. If
ownerlives in Prime Account state, the initializer bit that gates owner assignment must be read from that same effective state domain underdelegatecall. - Inventory every selector a user-storage contract can resolve through shared dispatcher logic.
The account does not implement this function itselfis not a safety argument if selector routing can still source the logic elsewhere. - Test direct and indirect initialization paths against already-funded live instances. DeltaPrime’s dangerous path was not the intended
initialize()entrypoint but the wrong initializer reachable through a different selector surface. - Deauthorize shared implementation code by context, not by convention. A beacon initializer should either be impossible to run from user-account context or provably unable to mutate user-account ownership even if it is reached.
- Treat owner-change aftermath as a separate risk surface. DeltaPrime’s new 24-hour withdrawal delay is a useful reminder that high-impact authority changes should trigger temporary withdrawal quarantine even if the nominal owner-change feature remains valid.
- Monitor for impossible production signals: post-launch
init()selector hits, bursts of account-owner changes, selector routes that were only meant for deployment, and ownership changes immediately followed by debt repayment plus collateral withdrawal. - Review emergency fixes for sibling selector / storage-namespace collisions, not just the first confirmed exploit path. DeltaPrime’s follow-on audit found two adjacent vectors during the same incident window.
Audit prompts
- Which selectors can a live Prime Account or proxy-style user vault resolve through shared beacon / facet / library routing, even if the storage contract itself does not define them?
- For every initializer-like function reachable anywhere in that routing graph, what exact storage slot proves
already initialized, and is it the same effective state domain that holdsowner, admin, or module-install rights? - Can a deployment-only helper still be invoked from production user storage through fallback
delegatecall, selector table lookup, or upgrade-era compatibility shims? - If two components both use
_initialized, are reviewers proving semantic equality of the guard, or just noticing the same variable name in different storage namespaces? - What prevents a shared beacon or implementation initializer from writing privileged fields into user-account state when executed under
delegatecall? - After an owner change, what controls delay or narrow immediate value extraction: withdrawal cooldowns, alerting, per-account pauses, or mandatory re-approval of risky call sequences?
- Which runtime alerts would distinguish
user legitimately migrated wallet ownershipfrommany accounts are suddenly changing owners through the same unusual selector pathquickly enough to matter? - During upgrades and incident-response patches, who re-runs the full selector-reachability map so deployment-time or legacy setup functions do not quietly become live again?
Lessons that should already be standard
- In proxy and diamond systems, selector reachability is part of authorization. A function can be absent from the user-facing contract and still be a live production admin surface.
Initializer protectedmeans nothing unless the guard authenticates against the same storage reality the privileged write will mutate.- Deployment-only code is not harmless just because the team is done deploying. If the selector still resolves in production, the authority still exists.
- Shared storage-slot discipline is not only about avoiding random corruption. It is also about making sure lifecycle guards and privilege writes talk about the same object under
delegatecall. - Ownership-transfer features deserve post-change friction when the account can custody meaningful collateral. A short withdrawal quarantine can turn ownership hijack from immediate drain into detectable incident.
- Incident review should search sideways, not just forward. Once one wrong-selector / wrong-slot initializer path exists, nearby selectors and sibling storage domains deserve immediate suspicion.
Internal linkages
- Core recurring patterns: unprotected-initializer-reinitialization, storage-layout-collision.
- Positive-design companions: implementation-and-singleton-direct-call-deauthorization and decommissioning-authority-extinguishment-discipline — DeltaPrime’s sharper lesson is that shared beacon logic and rollout-only selectors do not become safe by convention; they have to be sealed, unreachable, or explicitly retired once production accounts depend on them.
- Adjacent pattern: privileged-upgrade-abuse.
Control surface
-
The decisive leverage sat in selector routing from Prime Accounts into DiamondBeacon logic, the lingering production reachability of a deployment-only
init()function, the split_initializedstorage namespaces, and the fact that beacon code still wrote to the live Prime Accountownerfield. -
DeltaPrime did not lose funds because ordinary withdrawal math broke. It lost funds because a proxy-selector surface treated bootstrap authority as if it were safely dead when it was still reachable inside user vault storage.
-
Sources:
-
Last reviewed: 2026-07-28 UTC