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 calledtransferFrom()on the fake token, the token re-enteredVaultCore.mint()with a tiny USDT deposit. That re-entrantmint()triggered_rebase(), and_rebase()now saw the 20.5M DAI that the outermintMultiple()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’stransferFrom()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.
- A refactor removed the check that every asset in
- 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, whilemint()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.
- Origin lost the guarantee that each asset named in
- Exploit path:
- The attacker borrowed about 70,000 ETH through dYdX flash loans and swapped it into roughly 20.99M DAI and 7.85M USDT.
- The attacker called
OUSD.rebaseOptIn()so their address would participate in subsequent supply rebases. - They first called
VaultCore.mint()with about 7.5M USDT, receiving an ordinary OUSD position. - They then called
VaultCore.mintMultiple()with real DAI and a malicious token contract at0x47c3d84394043a4f42f6422accd27bb7240fdfe2. - Because the removed validation no longer rejected unsupported assets,
mintMultiple()accepted the fake token, computedpriceAdjustedTotalfrom the real DAI leg, and minted about 20.5M additional OUSD before finishing its transfer loop. - 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. - When the loop next called
transferFrom()on the fake token, that malicious contract re-enteredVaultCore.mint()with a tiny USDT amount. - 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. - OUSD’s supply therefore expanded by roughly that DAI amount even though the outer frame had already minted claims against the same DAI.
- 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.
- 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 balanceis 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 unwindcase; 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()andchangeSupply(). - The important sequencing detail is that both affected mint entrypoints were pre-transfer accounting paths.
mintMultiple()computedpriceAdjustedTotal, optionally ran_rebase(), and executedoUSD.mint(msg.sender, priceAdjustedTotal)before itssafeTransferFrom()loop; the re-enteredmint()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 insidemintMultiple()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 isexplicit 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()andchangeSupply(), which made the attacker’s own wallet balance expand before redemption. PeckShield’s accounting is a good memory aid here: the attacker had about28,002,000OUSD from three ordinary mint legs (7.5M USDT,20.5M DAI, and a tiny2K USDTre-entrant mint), but rebasing lifted that position to about33.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 transactionandvault-draining transactionwhen the underlying accounting primitive is already broken.
- This was not just a generic reentrancy bug. The preserved lesson is that
- 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
- Main exploit transaction highlighted by PeckShield:
- 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
-
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 outermintMultiple()frame had already pulled about20.5M DAIinto 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.
- The first dangerous fact was not yet
-
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 vaultandthis exact ledger may still treat it as newly recognized backingwere collapsed into one fact.
- The outer frame had already monetized the DAI once by minting OUSD explicitly against
-
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 OUSDof legitimate mint output to about33.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 stablecoinsouter frame minted OUSD against real DAI first, then later callback-time rebasing logic re-read the same already-live DAI as fresh backingrebasing balance presentation amplified one provenance mistake into wallet-balance inflation before redemptiontiny 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-complexityfailure, but a copied / gas-saving refactor that removed the supported-asset validation frommintMultiple(). 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
VaultCorecode. BothmintMultiple()andmint()performed supply-affecting work before their asset transfer completed:mintMultiple()computedpriceAdjustedTotal, optionally called_rebase(), minted OUSD, and only then entered thesafeTransferFrom()loop; the re-entrantmint()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 txinto 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, butcan 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 arrivedfromthis 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 tracemay already meanreserve-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 validatesis not a system property ifmintMultiple()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 balancesfromnew 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()orchangeSupply()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 proveno 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 UXor an accounting afterthought. Vault balance increaseddoes not provenew 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 mintcases. 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
-
The leverage sat in supported-asset validation for
mintMultiple(), the order ofmintversustransferFrom, the conditions under which_rebase()could run mid-transaction, and the fact that OUSD wallet balances were elastic views over global supply. -
Origin did not mainly fail because rebasing exists. It failed because the protocol let one real balance increase pass through both deposit accounting and supply-accounting truth without proving that those two frames were observing different economic events.
-
Sources:
- https://blog.originprotocol.com/what-weve-changed-since-the-ousd-attack-5894f2bd77cf
- https://peckshield.medium.com/origin-dollar-incident-root-cause-analysis-f27e11988c90
- https://decrypt.co/48478/ethereum-based-origin-dollar-hacked-for-estimated-7-million
- https://github.com/OriginProtocol/origin-dollar/commit/c9e3adc7a4f3bd1d8c2cf58135ee7c81479761fa
- https://raw.githubusercontent.com/OriginProtocol/origin-dollar/c9e3adc7a4f3bd1d8c2cf58135ee7c81479761fa/contracts/contracts/vault/VaultCore.sol
-
Last reviewed: 2026-08-02 UTC