DAO Maker Vesting Contracts Unauthenticated init() / emergencyExit() Drain (2021-09-04)

  • Name: DAO Maker vesting contracts unauthenticated init() / emergencyExit() drain
  • Date: 2021-09-04
  • Protocol: DAO Maker Vesting
  • Chain: Ethereum
  • Category: unprotected initializer / re-initialization ownership takeover / emergency withdrawal drain / shared implementation exposure
  • Loss: about $4M, commonly described as the combined value of drained DERC, CPD, CAPS, and SHO vesting inventories sold into DAI
  • Summary: DAO Maker’s vesting system used a shared implementation at 0xf17ca0e0f24a5fa27944275fa0cedec24fbf8ee2 whose init() path remained callable without authenticating who was allowed to perform setup. SlowMist’s decompilation of that implementation highlighted init() selector 0x84304ad7 as the unauthenticated ownership-setting surface. On 2021-09-04, the attacker called init() against multiple live vesting contracts, made themselves owner, then invoked emergencyExit() to pull out vested project tokens that should have remained locked for beneficiaries. One public onchain example is DERC-related tx 0xd5e2edd6089dcf5dca78c0ccbdf659acedab173a8ab3cb65720e35b640c0af7c, where the attacker called selector 0x84304ad7 against vesting agency 0x2fd602ed1f8cb6deaba9bedd560ffe772eb85940 and the receipt emitted OwnershipTransferred from 0x95626473b6782292d20f1b07a85a8b7f6ab63677 to attacker 0x2708cace7b42302af26f1ab896111d87faeff92f. The stolen allocations from Ternoa, Coinspaid, DeRace, and Showcase were then dumped through 1inch and other swap routes for DAI.
  • Trigger / entry point:
    • DAO Maker project vesting inventories were held in contract instances backed by a common vesting implementation.
    • The implementation exposed an init() function (selector 0x84304ad7) that public postmortems describe as unauthenticated.
    • That setup path could still rewrite privileged state on already-funded vesting instances.
    • The contract owner could call emergencyExit(), turning ownership capture directly into token withdrawal authority.
  • Root cause:
    • DAO Maker treated initialization as if it were a harmless deployment step rather than a root-of-trust ceremony.
    • init() did not prove that the caller was the legitimate deployer or authorized operator for the vesting instance.
    • Because init() could set or replace owner-like control, the attacker did not need a second bug after calling it; the contract itself then recognized the attacker as the party allowed to run emergencyExit().
    • The deeper failure was shared across several vesting instances at once because the same implementation and setup assumptions underlay multiple sale-token vesting contracts.
  • Exploit path:
    1. The attacker identified DAO Maker vesting contracts backed by implementation 0xf17ca0e0f24a5fa27944275fa0cedec24fbf8ee2, whose init() path public analyses describe as missing caller authentication.
    2. The attacker called init() on live vesting contracts that already held token balances for sale participants, making attacker-controlled state authoritative for those instances. In the publicly traced DERC path, tx 0xd5e2edd6089dcf5dca78c0ccbdf659acedab173a8ab3cb65720e35b640c0af7c hit vesting agency 0x2fd602ed1f8cb6deaba9bedd560ffe772eb85940 with selector 0x84304ad7 and immediately emitted OwnershipTransferred to the attacker.
    3. With ownership seized, the attacker called emergencyExit() on the affected vesting contracts.
    4. Public reporting attributes four realized drains to this sequence: 13.5M CAPS from 0x6e70c88be1d5c2a4c0c8205764d01abe6a3d2e22, 5M CPD from 0xd6c8dd834abeeefa7a663c1265ce840ca457b1ec across two exits, 1.44M DERC from 0xdd571023d95ff6ce5716bf112ccb752e86212167, and about 20.6M SHO from 0xa43b89d5e7951d410585360f6808133e8b919289.
    5. The attacker then sold the drained vesting inventory into DAI, with public reports highlighting 1inch swap transactions for CAPS, CPD, and DERC and a MetaMask Swap Router sale for SHO.
    6. Rekt also noted that after the first visible drain sequence, init() was used against two more vesting-related contracts, though those later token extractions appear to have been at least partly recovered by a separate address in a likely rescue / whitehat sequence.
  • What the attacker needed to be true:
    • Live vesting contracts had to remain re-initializable after deployment and funding.
    • init() had to be able to assign or overwrite owner-level authority rather than only configure harmless metadata.
    • emergencyExit() had to trust the current owner absolutely and transfer the held token inventory out immediately.
    • The affected vesting contracts had to already contain substantial sale-token balances.
    • There had to be enough market liquidity to liquidate the drained allocations before recovery measures could contain them.
  • Technical anatomy worth remembering:
    • This is a clean example of initialization itself being the exploit, not merely a prelude to some later privilege abuse. The DERC-linked init tx left onchain evidence of that directly: the attacker did not need an obscure storage collision or governance bundle; init() itself rewrote ownership on a live funded vesting path.
    • The shared-implementation aspect mattered: one bad initialization pattern replicated across several vesting contracts and let the same exploit recipe drain several unrelated project allocations.
    • emergencyExit() was not the original bug. It was the monetization path exposed once init() let the attacker rewrite who counted as owner.
    • The case is especially instructive because vesting systems often look operational rather than security-critical, yet here the vesting admin surface was a direct custody surface for locked token inventory.

Three governance-authority promotions this incident collapsed

  1. Some funded vesting instances and their setup / rescue selectors still existed as live authority objects

    • DAO Maker had already-funded vesting contracts deployed behind a shared implementation, and those contracts still exposed a callable init() path plus owner-only emergencyExit().
    • That was only the weakest fact: a vesting locker exists, it holds sale inventory, and constructor-like plus emergency verbs are still reachable.
  2. That setup path was over-promoted into canonical runtime owner truth

    • The protocol treated init() as if it still carried legitimate setup meaning on live funded instances instead of demanding that initialization authority had already been irreversibly spent.
    • Once the attacker called selector 0x84304ad7, the contracts themselves emitted OwnershipTransferred and accepted the attacker-authored owner state as ordinary administrative truth.
    • The real collapse was not merely missing access control; it was letting a deployment-time ceremony keep publishing who counted as the rightful vesting operator after funding.
  3. That counterfeit owner state still inherited full vesting-inventory withdrawal authority

    • After init() replay, emergencyExit() did exactly what the protocol had designed it to do for a trusted owner: transfer the locked project-token inventory out immediately.
    • The system did not distinguish between someone can write owner-like state, that state deserves to count as live operator authority, and that accepted operator authority may now sweep the whole vesting balance.
    • The drain transactions were therefore not a second independent auth bug; they were the intended emergency-admin path faithfully honoring already-counterfeit ownership truth.
  • Representative transactions and addresses:
    • Attacker EOA highlighted in public vesting analyses: 0x2708cace7b42302af26f1ab896111d87faeff92f
    • Likely later rescue / whitehat address highlighted by Rekt: 0xcf28556ee95be8c52ad2f3480149128cca51dac1
    • Vesting implementation contract: 0xf17ca0e0f24a5fa27944275fa0cedec24fbf8ee2
    • DERC-related vesting agency contracts highlighted by SlowMist: 0x2fd602ed1f8cb6deaba9bedd560ffe772eb85940, 0xdd571023d95ff6ce5716bf112ccb752e86212167
    • Public DERC init() ownership-takeover tx: 0xd5e2edd6089dcf5dca78c0ccbdf659acedab173a8ab3cb65720e35b640c0af7c
    • CAPS vesting contract / emergencyExit() tx: 0x6e70c88be1d5c2a4c0c8205764d01abe6a3d2e22 / 0xcb5be97496995d58da6f97491845040547b878e53a7b71f907a13408f3a54e5f
    • CPD vesting contract / emergencyExit() txs: 0xd6c8dd834abeeefa7a663c1265ce840ca457b1ec / 0x4c273c2403aafd97e4b553f0e381cf1c63e5f2efebbe2ded7642a06f2b68c879, 0xec2115ddb7f5020b2410978bb13cd23a9a38291d35be310d7825e7faeb6df1f4
    • DERC vesting contract / emergencyExit() tx: 0xdd571023d95ff6ce5716bf112ccb752e86212167 / 0x1692a57f19b5e8e4bc6a372ac3c83c77cd4a1ea78414377ea66d3d59f4a7d2b7
    • SHO vesting contract / emergencyExit() tx: 0xa43b89d5e7951d410585360f6808133e8b919289 / 0xdd0176475165b83c702d49a876d4dc888b73477ad8833582c72aa6ca5e0bacc3
    • Representative token-sale swap txs: CAPS 0xbf38346aacf261f5e169a87ed874c33c21efb060c4a393e2b1443a3ac5d6e3fd, CPD 0x3436af2c84d67254a4b81adc350c91d1b98ae52b2ff84645d14d4245c2d08c27, DERC 0xc586a6b94e09556abf46ae3aa8cffa8e46dfcb0c22bce0b024d5e01743ceba9e, SHO 0x76163daf6cf0c815c02fb1a98f5c6283ee7a922cbad41218eb7a6452c91824c8
  • Response / mitigation:
    • DAO Maker and affected projects publicly acknowledged the incident, while third-party analysts catalogued the impacted vesting inventories and sale transactions.
    • Rekt observed that after the first visible drain sequence, a second address (0xcf28556ee95be8c52ad2f3480149128cca51dac1) used the same init() -> emergencyExit() primitive against additional vesting-related contracts, then returned extracted tokens and transferred ownership, which strongly suggests belated rescue / whitehat containment once the initialization bug was understood.
    • The lasting lesson was that vesting instances and their initialization flows must be treated as privileged custody systems, not as low-risk distribution utilities.

Pattern tags

  • live funded vesting instances still accepted constructor-like setup, so initialization itself became the ownership-takeover primitive
  • shared implementation multiplied blast radius because one unauthenticated init pattern exposed several unrelated token-sale inventories
  • emergency withdrawal logic was not independently broken; it faithfully monetized attacker-owned admin state after init replay
  • vesting admin and deployment ceremony were effectively custody controls, even though the product surface looked like token distribution infrastructure

Why the exploit worked

  • DAO Maker left a constructor-like trust ceremony alive on production vesting contracts that already held user-facing token inventory. That collapsed the line between deployment-time setup and runtime admin authority. The DERC-linked init() receipt makes the collapse painfully concrete: the same call that should only ever have belonged to deployment emitted a live OwnershipTransferred event in production.
  • The decisive bug was not subtle storage aliasing or governance capture. init() itself could still assign owner-like authority on live instances, so the attacker could make the contracts truthfully believe they were the authorized operator.
  • Once ownership moved, emergencyExit() became an intended privileged withdrawal path rather than a second exploit step. The drain path was therefore as strong as the protocol’s own emergency-admin design.
  • The shared implementation made the incident operationally worse: one broken initialization assumption repeated across multiple project vesting contracts, so the attacker could run the same playbook against several token lockers in sequence.

Detection and audit pivots

  • Enumerate every funded vesting or lockup instance and ask whether any init, initialize, setup, or owner-assignment selector remains callable after deployment. Treat distribution infra as a custody audit surface, not a back-office utility.
  • Compare implementation, clone, and instance addresses separately. A safe-looking vesting front contract is not enough if a shared implementation still exposes constructor-refactor helpers that can rewrite privileged state.
  • Trace every path from owner capture to immediate monetization: emergencyExit(), rescue functions, arbitrary token sweeps, beneficiary replacement, and pause or migration helpers. If init replay gives access to any of them, the exploit is economically complete.
  • Monitor for post-launch initialization behavior on live lockers: fresh Initialized-style events, owner changes shortly before emergency exits, or repeated init selectors hitting already-funded contracts should already page responders.

Failure-prevention checklist

  • Make deployment and initialization atomic for vesting, escrow, and token-lock contracts. A locker should never be funded before the operator set and initialization guard are irreversibly anchored.
  • Permanently extinguish deployment-only setup selectors on live instances and on shared implementations. If clones or proxies need setup helpers, the code objects backing them should still be directly inert once production funding exists.
  • Treat emergencyExit() and related rescue paths as balance-sheet withdrawal authority. They need the same threat model as treasury transfer functions because ownership capture immediately turns them into drain rails.
  • Add invariants proving that owner/admin can only be set once, that init cannot be replayed through alternate selectors or shared implementations, and that funded vesting contracts can never revert to a fresh-looking state.
  • Wire production monitoring around funded uninitialized contracts, unexpected owner changes, and any direct call into implementation addresses that should only ever serve as passive code templates.

Audit prompts

  • Which exact address is supposed to anchor trust for each live vesting instance, and what proves that setup authority cannot be replayed after funding?
  • Can any implementation, clone template, migration helper, or fallback-routed selector still write owner or beneficiary authority on a contract that already holds user assets?
  • If an attacker became owner for one block, what immediate value-moving verbs become available: emergencyExit(), token rescue, beneficiary rewrite, migration, or arbitrary execution?
  • Are deployment scripts guaranteeing one-step initialization everywhere the system was rolled out, or merely assuming an operator will race nobody to complete setup later?
  • What telemetry would tell operators that a vesting contract just behaved like a fresh deployment after months of holding balances?

Lessons that should already be standard

  • Vesting systems are not low-risk utility contracts. If they custody locked inventory, their admin and setup paths are part of the protocol’s live custody surface.
  • Emergency withdrawal functions do not need their own bug to become catastrophic; they only need ownership to move once.
  • Shared implementations amplify boring initialization mistakes into portfolio-wide incidents. Reused code means reused blast radius.
  • Constructor logic that survives as a callable runtime function is still privileged code, no matter how mundane the product wrapper sounds.
  • A funded contract whose initialization state is still ambiguous is already compromised in principle, even before an attacker proves it in production.

Why this incident matters

  • It is a compact, high-signal example of unprotected initializer turning directly into custody loss without any need for fancy proxy mechanics or multi-step governance abuse.
  • It preserves the multi-instance blast-radius problem: one flawed vesting implementation pattern exposed several separate token-sale inventories at once.
  • It also shows why emergency admin functions deserve the same scrutiny as withdrawal or upgrade functions, because once ownership is captured they usually become the fastest drain path.

Internal linkages

Control surface