Origin Dollar OUSD mintMultiple() Missing-Validation Reentrancy / Supply Inflation (2020-11-16)

  • Name: Origin Dollar OUSD mintMultiple() missing-validation reentrancy / supply inflation
  • Date: 2020-11-16
  • Protocol: Origin Dollar (OUSD)
  • Chain: Ethereum
  • Category: balance-delta provenance failure / unsupported-asset admission / reentrancy / rebasing-stablecoin supply inflation
  • Loss: about 7.7M, commonly cited as 11,809 ETH plus 2,249,821 DAI
  • Summary: A gas-saving refactor removed a supported-asset validation from Origin Dollar’s VaultCore.mintMultiple() path. The attacker abused that gap by passing a malicious ERC20-like token alongside real DAI. mintMultiple() minted OUSD for the real stablecoin amount first and only then began pulling assets into the vault. When the function later called transferFrom() on the fake token, the token re-entered VaultCore.mint() with a tiny USDT deposit. That re-entrant mint() triggered _rebase(), and _rebase() now saw the 20.5M DAI that the outer mintMultiple() frame had already pulled into the vault. The real DAI only arrived once, but two accounting paths monetized it: the outer frame had already minted OUSD explicitly against that DAI, and the later re-entrant frame re-labeled the same already-arrived DAI as fresh backing for a supply-expanding rebase. Because the attacker had opted into rebasing, their OUSD balance inflated with the global supply and could then be redeemed for real vault assets.
  • Trigger / entry point:
    • A refactor removed the check that every asset in VaultCore.mintMultiple() had to be supported.
    • mintMultiple() minted OUSD before pulling the listed assets into the vault.
    • The transfer loop later called safeTransferFrom() on an attacker-chosen fake token, and that token’s transferFrom() re-entered the vault.
    • The re-entrant VaultCore.mint() path called _rebase() before its own transfer, letting it observe the outer call’s already-received DAI as fresh vault value.
  • Root cause:
    • Origin lost the guarantee that each asset named in mintMultiple() was one of the vault’s supported stablecoins, so an attacker-controlled token contract entered a mint path that assumed passive ERC-20 behavior.
    • The vault also let deposit-side accounting and rebasing-side accounting see the same balance increase in different frames of the same transaction.
    • mintMultiple() minted claims before asset transfers finished, while mint() and _rebase() still treated live vault balances as authoritative backing.
    • Because OUSD used elastic rebasing balances, the accounting mistake did not merely overmint one position: it also expanded the attacker’s wallet balance through changeSupply() once the rebase saw the duplicated value.
  • Exploit path:
    1. The attacker borrowed about 70,000 ETH through dYdX flash loans and swapped it into roughly 20.99M DAI and 7.85M USDT.
    2. The attacker called OUSD.rebaseOptIn() so their address would participate in subsequent supply rebases.
    3. They first called VaultCore.mint() with about 7.5M USDT, receiving an ordinary OUSD position.
    4. They then called VaultCore.mintMultiple() with real DAI and a malicious token contract at 0x47c3d84394043a4f42f6422accd27bb7240fdfe2.
    5. Because the removed validation no longer rejected unsupported assets, mintMultiple() accepted the fake token, computed priceAdjustedTotal from the real DAI leg, and minted about 20.5M additional OUSD before finishing its transfer loop.
    6. The outer mintMultiple() transfer loop then pulled the real DAI into the vault, so the DAI backing was already live in vault balances even though the outer call had already spent that same DAI once by minting OUSD against it.
    7. When the loop next called transferFrom() on the fake token, that malicious contract re-entered VaultCore.mint() with a tiny USDT amount.
    8. The re-entrant mint() called _rebase() before its own tiny transfer could matter, and _rebase() now saw the outer frame’s already-arrived 20.5M DAI as fresh vault backing.
    9. OUSD’s supply therefore expanded by roughly that DAI amount even though the outer frame had already minted claims against the same DAI.
    10. Because the attacker had opted into rebasing and already held most of the supply, their balance inflated from roughly 28.0M legitimately minted OUSD to about 33.269M OUSD.
    11. The attacker redeemed that inflated OUSD against vault assets, draining about 19.5M DAI, 9.4M USDT, and 3.9M USDC before unwinding back into ETH and repaying the flash loan.
  • What the attacker needed to be true:
    • mintMultiple() had to accept an attacker-controlled unsupported token in its asset array.
    • The vault had to mint OUSD before the full asset-transfer loop completed.
    • A later re-entrant path had to be able to trigger _rebase() while fresh assets from the outer frame were already sitting in the vault.
    • The attacker needed to be opted into rebasing so the supply expansion would actually increase their wallet balance.
    • The inflated OUSD had to remain redeemable for real stablecoins held by the vault and strategies.
  • Technical anatomy worth remembering:
    • This was not just a generic reentrancy bug. The preserved lesson is that live vault balance is not source-specific accounting. Once a later frame can observe a deposit that an earlier frame already monetized, the same backing can authorize two different claim paths.
    • Relative to Akropolis and Grim, Origin preserves the rebasing-balance cousin of the same accounting family but with the order reversed. Akropolis is the clean two-frame one real deposit lands below, one outer frame remints on unwind case; Grim is the repeated-checkpoint-reset multi-frame vault version; Origin instead let the outer frame monetize first, then let a later re-entrant frame reinterpret that same already-arrived DAI as fresh backing for _rebase() and changeSupply().
    • The important sequencing detail is that both affected mint entrypoints were pre-transfer accounting paths. mintMultiple() computed priceAdjustedTotal, optionally ran _rebase(), and executed oUSD.mint(msg.sender, priceAdjustedTotal) before its safeTransferFrom() loop; the re-entered mint() also ran _rebase() before pulling its own tiny USDT deposit. That means the nested leg was not mainly valuable because it delivered meaningful new assets. Its job was to reopen rebasing logic at exactly the moment when the outer frame’s DAI was already visible in vault balances but had not been quarantined from later supply-accounting logic. PeckShield’s walkthrough even renders the fake-token leg inside mintMultiple() as zero-valued, which is a useful classifier: the hostile token was primarily a callback carrier, not a second source of economic input.
    • A useful classifier shortcut is: Akropolis and Grim are bottom-of-stack real deposit, earlier-frame remint on unwind; Origin is explicit mint first, later frame rereads the same backing through rebasing logic.
    • OUSD’s elastic-supply design amplified the damage. The duplicated DAI did not only overmint in one call frame; it also passed through _rebase() and changeSupply(), which made the attacker’s own wallet balance expand before redemption. PeckShield’s accounting is a good memory aid here: the attacker had about 28,002,000 OUSD from three ordinary mint legs (7.5M USDT, 20.5M DAI, and a tiny 2K USDT re-entrant mint), but rebasing lifted that position to about 33.269M OUSD, making the missing millions pure balance-presentation inflation sourced from the same already-counted DAI.
    • The postmortem is blunt that the decisive bug was introduced during a gas-saving refactor that copied functionality without copying the validation check.
    • The six-minute gap between an earlier failed attempt and the successful exploit shows how little time there can be between weird failing transaction and vault-draining transaction when the underlying accounting primitive is already broken.
  • Representative transactions and contracts:
    • Main exploit transaction highlighted by PeckShield: 0xe1c76241dda7c5fcf1988454c621142495640e708e3f8377982f55f8cf2a8401
    • Malicious token / attack-helper contract identified by PeckShield: 0x47c3d84394043a4f42f6422accd27bb7240fdfe2
    • Fix commit / post-incident hardening branch cited by Origin: c9e3adc7a4f3bd1d8c2cf58135ee7c81479761fa
  • Response / mitigation:
    • Origin paused minting and redeeming, investigated the exploit, and later published a full compensation plan for OUSD holders affected at the time of the incident.
    • The protocol restored the missing validation, added explicit reentrancy checks around the vault, and commissioned additional audits and formal-verification work.
    • Origin also added faster pause tooling and better transaction monitoring after concluding it had only a narrow window between the first anomalous attempt and the successful drain.

Three backing-authority promotions this incident collapsed

  1. A real vault-balance increase had already become visible inside the transaction

    • The first dangerous fact was not yet OUSD is insolvent; it was that the outer mintMultiple() frame had already pulled about 20.5M DAI into the vault, so a large backing-looking balance delta was now sitting in raw vault inventory before the whole mint flow had finished settling.
    • At that point the protocol still only had a live balance observation, not a proof that some second accounting consumer was entitled to spend that observation as fresh supply authority.
  2. That same visible balance delta was still promoted into fresh backing truth for a different ledger

    • The outer frame had already monetized the DAI once by minting OUSD explicitly against priceAdjustedTotal.
    • The re-entrant mint() then reopened _rebase() while that same DAI was visible in vault balances, and _rebase() / changeSupply() promoted the already-counted inventory into fresh global-supply backing again.
    • This is the decisive split the incident preserves: value is visible in the vault and this exact ledger may still treat it as newly recognized backing were collapsed into one fact.
  3. That accepted rebasing-side truth still deserved immediate redemption authority

    • Once the attacker had opted into rebasing and concentrated most of the supply, the duplicated backing story did not stay an internal accounting wart.
    • It inflated the attacker’s wallet from about 28.002M OUSD of legitimate mint output to about 33.269M OUSD, and the rest of the system still honored that rebasing-expanded balance as fully redeemable against real DAI, USDT, and USDC.

The durable lesson is that vault inventory moved, that movement still counts as fresh backing for this ledger, and claims printed from that accepted story still deserve live reserve-release authority are three different promotions. Origin failed all three in one transaction.

Pattern tags

  • gas-saving refactor removed supported-asset validation from mintMultiple(), so attacker-authored transfer semantics entered a path that assumed known stablecoins
  • outer frame minted OUSD against real DAI first, then later callback-time rebasing logic re-read the same already-live DAI as fresh backing
  • rebasing balance presentation amplified one provenance mistake into wallet-balance inflation before redemption
  • tiny re-entrant USDT deposit mattered as an accounting-frame reopen, not as economically meaningful new backing

Why the exploit worked

  • Origin’s own postmortem is useful on the first boundary: this was not a mysterious DeFi-complexity failure, but a copied / gas-saving refactor that removed the supported-asset validation from mintMultiple(). Once that gate disappeared, an attacker-controlled fake token could sit inside a path that was only safe for known stablecoins with passive transfer semantics.
  • The vulnerable accounting order is also unusually crisp in the published VaultCore code. Both mintMultiple() and mint() performed supply-affecting work before their asset transfer completed: mintMultiple() computed priceAdjustedTotal, optionally called _rebase(), minted OUSD, and only then entered the safeTransferFrom() loop; the re-entrant mint() path also ran _rebase() before its own transfer. That meant the attacker did not need the nested call to bring meaningful new value. They needed it to reopen supply logic while the outer frame’s DAI was already sitting in the vault.
  • The core invariant failure was therefore one real DAI inflow, two balance-authority stories. The outer frame had already spent that DAI once by minting OUSD explicitly; the later re-entrant frame then let _rebase() / changeSupply() spend it again as if new backing had arrived.
  • OUSD’s opt-in rebasing model converted that accounting mistake into a more dangerous payout shape than a normal over-mint. Because the attacker had opted into rebasing and already held most of the supply, the second counting event inflated the attacker’s wallet balance directly before redemption.
  • The postmortem’s operational detail also matters: Origin says a failed attempt preceded the successful exploit by about six minutes. That is a reminder that once a live accounting seam exists, attackers often only need one quick iteration to turn odd failed tx into a reserve-draining retry.

Detection and audit pivots

  • Treat every batched mint path that prices totals before transfers as a solvency review target, especially when a later loop iterates caller-supplied assets. The sharp question is not only are amounts validated, but can one leg reopen accounting while another leg's backing is already live.
  • Review copied or gas-optimized entrypoints for security-check drift, not just functional equivalence. Origin’s own root cause was literally copied functionality without copied validation, which is exactly the kind of regression normal happy-path tests miss.
  • If multiple entrypoints can call _rebase(), changeSupply(), or other balance-presentation logic, test whether one path can mint explicitly while another same-transaction path rereads the same live backing into supply expansion.
  • Distinguish new assets arrived from this exact entrypoint delivered new assets that have not already been consumed by another ledger. Origin is a strong reminder that rebasing and presentation math are part of solvency accounting, not just UX.
  • Simulate the economically realistic attacker posture, not only minimal PoCs: opt the attacker into rebasing, let them hold most of supply, and ask whether a supply expansion bug becomes a direct wallet-balance and redemption bug.
  • Operationally, treat anomalous failed mint / rebase attempts as page-worthy when they touch unsupported assets or unusual asset arrays. Origin’s six-minute gap is the warning that failed exploit trace may already mean reserve-draining retry inbound.

Failure-prevention checklist

  • Enforce supported-asset admission on every mint entrypoint, especially copied or gas-optimized batch paths. The single-asset path validates is not a system property if mintMultiple() or a helper variant can bypass the same allowlist.
  • Do not let supply-affecting logic run before receipt-affecting logic settles. If OUSD is being minted against incoming assets, the protocol should first prove which assets were actually received by this exact call and only then let mint or rebase machinery consume that receipt.
  • Separate ambient vault balances from new backing recognized by this action. Raw vault inventory should not become fresh mint or rebase authority merely because it is visible mid-transaction.
  • Keep explicit mint accounting and rebasing / presentation accounting bound to one backing object. A deposit that already justified oUSD.mint(...) should not later be re-read by _rebase() or changeSupply() as if a second economic event occurred.
  • Treat unsupported or callback-capable assets as hostile by default. If a path is only safe for known stablecoins with passive transfer semantics, enforce that policy in code instead of relying on convention.
  • Model rebasing opt-in as part of the payout surface. If balance-presentation logic can inflate a holder’s wallet directly, put conservative guards, caps, or pause authority around abnormal same-transaction supply jumps.
  • Fuzz nested traces that mix real assets with fake assets and tiny re-entrant deposits. The invariant to assert is one economically real inflow can create at most one redeemable increase in aggregate OUSD claims.
  • Alert on strange failed mint / rebase attempts and unusual asset arrays quickly enough to pause before a retry. Operational reaction time was part of the loss surface here, not just contract correctness.

Audit prompts

  • Which mint, batch-mint, rebasing, or supply-sync entrypoints can each create or expand claims, and what exact receipt object proves they are all consuming different economic events?
  • Can any path mint claims before transfer completion, then let a later callback-capable asset or sibling entrypoint reopen supply logic while those just-received assets are already visible in vault balances?
  • If a user passes an unsupported asset, where is that path rejected, and is the rejection guaranteed across all helpers, refactors, and copied code paths?
  • Does _rebase() or any balance-presentation function consume raw vault balances directly, or does it consume only explicitly recognized backing that has not already been spent by another ledger?
  • If one deposit has already minted claims, what invariant prevents another same-transaction path from reinterpreting that same backing as fresh supply-expansion authority?
  • What happens if the attacker both opts into rebasing and concentrates most of the outstanding supply before triggering the bug? Does the exploit become a direct wallet-balance inflation event rather than only an accounting mismatch?
  • Are tests proving only reentrancy reverted, or do they also prove no mix of mint, batch mint, and rebase can turn one real inflow into two redeemable stories?
  • Which monitoring rules would page operators on the first failed attempt: unsupported asset arrays, abnormal same-block supply jumps, rebase spikes, or repeated mint failures from the same address?

Lessons that should already be standard

  • Copied functionality is copied security responsibility. A gas-saving refactor that drops one validation check can silently create a whole new trust boundary.
  • Rebasing and balance presentation are solvency code once they can change redeemable wallet balances. They are not just UX or an accounting afterthought.
  • Vault balance increased does not prove new backing arrived for this ledger. Ambient inventory is not a safe source of mint authority unless the protocol can prove provenance.
  • Batch entrypoints are usually where invariants go to die. Teams often reason carefully about the simple path and then accidentally give the optimized path looser admission or sequencing rules.
  • One real inflow should support one redeemable claim story. If explicit mint logic and rebase logic can both tell coherent stories about the same backing, the attacker will cash out both.
  • Unsupported-token admission bugs are especially dangerous when the admitted asset can reopen live supply logic. The hostile token often matters less as value and more as timing control.
  • A six-minute gap between failed attempt and successful drain is enough to matter. Operational detection and fast pause paths belong in the design story for high-value mint systems, not only in the postmortem.

Why this incident matters

  • It is a sharp example of balance-provenance failure inside a rebasing token system: the same stablecoin deposit first justified an explicit mint and then justified a supply rebase.
  • It preserves a different monetization rail than the Akropolis / Grim-style same deposit counted twice for share mint cases. There, the extra claim is another share mint on unwind; here, the second monetization path flowed through elastic-balance presentation and global supply expansion before redemption.
  • It is also a reminder that unsupported-asset admission bugs are especially dangerous when they sit inside paths that later call back into live-supply or NAV machinery.

Internal linkages

  • Core recurring pattern: balance-delta-provenance-failure — Origin is the rebasing-balance cousin of that family: the outer frame already minted against real DAI, and a later re-entrant frame then re-read that same now-live backing through _rebase() / changeSupply() as if fresh value had arrived.
  • Asset-admission lens: interface-conformance-semantic-compatibility-failure — the exploit only became possible because mintMultiple() accepted an attacker-controlled unsupported token in a path that assumed a supported asset with safe transfer behavior.
  • Reserve / claim companion: reserve-backed-claim-truth-security-object — the healthy design question is how one real stablecoin inflow should stay bound to one minted claim path instead of later becoming ambient backing for rebasing logic to consume again.
  • Presentation-layer companion: reward-recognition-presentation-and-collateral-separation — Origin is a good reminder that rebasing and balance presentation are still solvency code once they can reinterpret recent backing changes into wallet-balance truth.
  • Adjacent pattern: reentrancy.

Control surface