Makina's $1.8M Loss and How Olympix Would Have Prevented It
Date: January 20, 2026
Chain: Ethereum
Loss: about $1.8M net, after Makina recovered about $3.3M of the 5,108,973 USDC drained from the DUSD/USDC Curve pool
Class: permissionless accounting that let a flash-loaned spot value set a pool's oracle price
At 03:40:23 UTC on January 20, 2026, a wallet deployed an unverified contract on Ethereum built for one job, pushing the price Makina's DUSD/USDC Curve pool assigned to DUSD far above what DUSD was worth. It never got to use it. Twelve seconds later, in the very next block, an MEV searcher who had decompiled that contract ran the same attack first, and a single transaction pulled 5,108,973 USDC out of the pool. About 1,299 ETH of it ended up with the block builder and the Rocket Pool validator that proposed the block.
Makina's own vaults, which it calls Machines, never lost a dollar of their holdings. The money came from liquidity providers in an external Curve pool that priced DUSD off the Machine's exchange rate, and that rate could be moved inside one transaction because anyone could ask the Machine to revalue a Curve position whose value a flash loan could inflate for a few seconds. Within a week, Makina got back 920.7 ETH from the builder, worth about $2.75M at CoinGecko's price that day, 157.1 ETH from the validator, worth about $464K, and 104,491 USDC in fees the attack had paid into its own Machine, which leaves liquidity providers about $1.8M short of what the pool lost. The drain itself was set in motion by two public functions that trusted whatever number the accounting returned.
Machines, Calibers and a Number Called AUM
Makina describes itself as a DeFi execution engine. Strategy operators run Machines, and depositors hold Machine tokens whose value tracks everything the Machine owns. Dialectic, Makina's first operator, ran three of them: DUSD, DETH and DBIT. The DUSD Machine went live in Makina's pre-launch season on September 29, 2025, and Rekt put Makina's total value locked at about $100M when the exploit hit.
A Machine doesn't hold its positions directly. Each chain where a strategy operates has a Caliber, a contract that holds the positions and keeps their values in storage. To price a position, a Caliber runs an accounting instruction, a pre-approved Weiroll script (a small program built from chained contract calls) that reads on-chain data and returns how much the position is worth. According to CertiK's analysis, the instructions a Caliber may run are verified against a Merkle root before execution, so only scripts the operator and risk manager approved can be used.
The Caliber adds its position values into its assets under management, or AUM. The Machine adds every Caliber's AUM into its own total, and the exchange rate of the Machine token is that total divided by the token supply. Makina's own redemption contract protected the Machine from a spiked rate by settling every exit at the lower of two prices, the one when a user asked to redeem and the one when the operator settled. That protection lived inside Makina, though, and DUSD also traded outside it.
To give DUSD holders a place to sell, Dialectic moved DUSD liquidity into Curve pools in late October 2025. Makina's post-mortem explains that those pools used an oracle that read the Machine's exchange rate directly, so the pool's liquidity stayed concentrated around the fair price. The oracle took the current Machine state and relayed it to the pool as is, in the same block. Whatever rate the Machine reported, the DUSD/USDC pool would trade at.
A Position Priced by Asking a Pool What It Would Pay
One of the DUSD Machine's positions was liquidity in Curve's MIM-3CRV pool. Its accounting instruction valued that position by calling calc_withdraw_one_coin(), a Curve function that answers a hypothetical question, how many 3CRV tokens would come out if this position were withdrawn into a single coin right now. The answer depends on the pool's current balances, and a large enough deposit can move those balances for the length of one transaction.
Calling calc_withdraw_one_coin() is like valuing your house by asking the one buyer currently standing in the driveway, and the attacker could bring their own buyer. Makina's post-mortem names three conditions that let that inflated answer travel all the way to the Curve pool. Position values could be updated by anyone, by design. The Caliber's AUM and the Machine's AUM could both be updated in the same block, so a flash loan could carry the manipulation through both. And the Curve pool's oracle passed the resulting exchange rate straight into its pricing.
The first two conditions sit in two public functions. From Makina's public makina-core repository at the v1.0.0 tag, the version the DUSD Machine ran on January 20, this is Caliber.accountForPosition:
The function is external and guarded only against reentrancy. It checks that the position exists and isn't part of a group, and then hands the instruction to _accountForPosition, which confirms the script is an approved accounting instruction, executes it, and writes whatever value comes back into storage as the position's new value along with the current timestamp. Nothing checks who is calling, how far the new value moved from the last one, or what state the pools the script reads are in at that moment. The Machine side has the same shape. From Machine.sol at the same tag:
updateTotalAum() is callable by anyone while the Machine isn't in recovery mode, and the library function it calls stores the freshly summed AUM as the Machine's last total along with the time. The audited threat model treated every approved accounting instruction as non-manipulable, which made a public refresh look harmless, and once one instruction read a spot value, the public refresh became the doorway. A value that anyone can move within one transaction shouldn't be able to flow, in that same transaction, into a price that other people trade against.
Twelve Seconds and Two Laps
The original attacker, 0x2F93, deployed the attack contract in block 24273361 at 03:40:23 UTC. The searcher at 0x935b ran its own version in block 24273362 at 03:40:35 UTC, and Blockscout's record of that transaction shows how the money moved. The searcher borrowed 160,590,920 USDC from Morpho and 119,409,079 USDC from Aave, about 280M USDC in flash loans, all of it repaid before the transaction ended.
The searcher added 100M USDC of liquidity to the DUSD/USDC pool and bought about 9.2M DUSD with another 10M USDC at the pool's normal price. It then pushed 170M USDC into Curve's DAI/USDC/USDT pool and fed part of the resulting 3CRV into MIM-3CRV, which skewed the balances the DUSD Machine's accounting script was about to read. With the pools distorted, it called accountForPosition() on the Caliber and updateTotalAum() on the Machine. According to CertiK and QuillAudits, the DUSD share price jumped from about 1.011771 to about 1.331577.
The Curve pool's oracle picked up the new rate immediately. The searcher sold its 9.2M DUSD back for 12,785,692 USDC, removed its liquidity for 100,666,132 USDC, unwound the MIM-3CRV and 3pool positions, and then ran the whole lap a second time, collecting 12,847,360 USDC from a second DUSD sale and 98,809,789 USDC from a second withdrawal. Across both laps the pool took in 220M USDC and paid out 225,108,973 USDC, a net loss of 5,108,973 USDC to the pool's liquidity providers.
After repaying both lenders, the searcher swapped 4,244,312 USDC for 1,299.18 WETH on Uniswap V3 and unwrapped it. The difference between the pool's loss and that swap was the cost of running the attack, which Makina's recovery update puts at about $884K in Curve fees and flash-loan fees. Makina's post-mortem records that the searcher kept 0.13 ETH and paid 1,299.04 ETH to the block builder at 0xa6c2, which sent 276.32 ETH to the block's proposer, a Rocket Pool node operator, and 1,023 ETH to a builder address at 0xbed.
Recovery Mode, Safe Harbor and a Validator Found Through Kraken
Hypernative's monitoring alerted the Makina team at 03:40:35 UTC, the same second the transaction landed. The team became aware of it at 04:26, SEAL911 opened a war room two minutes later, and at 05:46 the Security Council put the DUSD Machine into Recovery Mode, pausing exchange-rate updates, deposits and redemptions. DETH and DBIT followed at 06:04, and Makina's first public notice at 06:42 asked liquidity providers to leave the Curve pool.
By 09:26 Makina had sent on-chain messages to the two builder addresses holding 1,023 ETH, and at 11:08 to the validator address holding 276.32 ETH. The builder at 0xbed answered on Blockscan chat the next afternoon and, at 13:15 UTC on January 22, returned 920.7 ETH, keeping 10% as a bounty under the SEAL Whitehat Safe Harbor policy. The validator took longer to reach. Makina sent 10.276 USDC to a Kraken deposit address linked to it so that an exchange email might draw attention to its message, established contact through an off-chain channel on January 22, and received 157.1 ETH on January 23. About 118 ETH of the validator's share had already been absorbed into rETH backing under Rocket Pool's contract logic and couldn't be returned.
Makina's recovery update counts 1,077.8 ETH recovered, worth 3,184,252 USDC when it was transferred, and distributed pro rata to liquidity providers using a snapshot taken at block 24273361, the block before the exploit. Dialectic returned 104,491 USDC in fees the DUSD Machine had earned from the attack's own volume through MIM-3CRV, one trader who had profited from the dislocated pool after the exploit agreed to return 229,963 USDC, and Makina and Dialectic pledged half of their revenue from all three Machines until the remaining users are repaid. On July 31, 2026, Makina minted Recovery Notes, NFTs that record each user's outstanding claim and the repayments made against it, and sent them to the affected wallets.
Two Fixes and a Lesson About Scope
Makina shipped two changes in its v1.1 upgrade, which went live on January 26 after ChainSecurity reviewed it. Accounting can now be restricted to approved keepers, and in v1.1 accountForPosition carries a new modifier, onlyAccountingAuthorized, that reverts for any caller the Machine hasn't authorized. A second change lets risk managers cap how far the exchange rate can move in a short period, so an abnormal position value can't propagate to outside pools. Makina also removed every Curve integration from its approved instructions.
Makina's contracts had been reviewed by Enigma Dark, Sigma Prime, ChainSecurity and OtterSec, and Cantina ran a capture-the-flag competition, according to Rekt, which also notes that Cantina's rules listed losses from oracle or liquidity-pool manipulation as out of scope. Makina's post-mortem describes where the gap was. The reviews focused on the core system under the assumption that approved accounting instructions couldn't be manipulated, and the downstream effect of manipulated accounting on external integrations wasn't a primary focus. The team also wrote that the large number of instructions added at the initial root publication made errors more likely and the review less focused, and it now asks for more automated tooling earlier in the instruction process.
Every piece behaved as its reviewers expected. The accounting instruction read a real Curve value, the Caliber stored what the instruction returned, the Machine summed what its Calibers reported, and the Curve oracle relayed the rate it was given. The loss lived in the path between them, which no single review owned.
How Olympix Follows a Price From the Pool It Reads to the Pool It Sets
Olympix reads a protocol the way an attacker does, as states, transitions and entry points, and it takes in the context around the code, the integrations, the oracles and the pools the contracts depend on, to infer the invariants the system relies on. In Makina, the state that matters is the Machine's exchange rate and the position values that feed it, and the entry points that matter are every function that can rewrite them. Two of those, accountForPosition() and updateTotalAum(), are open to any caller.
A contract's state space looks infinite but isn't, and the engine uses signals to decide which paths are worth attacking. The signal here is a value derived from another protocol's live pool balances, calc_withdraw_one_coin() on MIM-3CRV, written into storage by a function anyone can call and then summed into a price by a second function anyone can call, with no time-weighting, no delay and no bound on how far it can move. Olympix has flagged this class before in other codebases, prices that an attacker can set by moving a spot balance first. From that signal the engine narrows from every path, to the feasible ones that end in a change to the exchange rate, to the ones where the same transaction can both move the source pool and act on the new rate.
The invariant fits in one sentence: the DUSD exchange rate an external pool trades against must not change by more than ordinary accounting drift within a single transaction that also trades against it. The engine attacks that rule by executing the flagged paths with a lender-sized balance available, treating Curve's pool state as something a caller can push, and looking for a sequence that ends with the caller holding more value than it started with.
The counterexample for this exploit would contain the sequence that hit Makina on January 20: buy DUSD at the fair rate, distort the MIM-3CRV and 3pool balances, call accountForPosition() on the MIM-3CRV position and updateTotalAum() on the Machine, sell DUSD back at the inflated rate and unwind. It would state the minimal conditions, a permissionless accounting call, a same-block AUM refresh and a spot-reading instruction, and it would come with a generated Forge test that replays the sequence against forked state and asserts that the pool pays out more USDC than it took in. That's the January 20 drain produced in development instead of on mainnet.
Olympix runs automatically in development, at every merge and before release, so the moment that would have surfaced this is the one where the MIM-3CRV accounting instruction was approved alongside a Curve pool whose oracle read the Machine's rate. It would have surfaced again at every later release while both stayed live.
Accounting That Moves Faster Than Anyone Trades
Makina's Machines held up because their own redemptions settled at the lower of two prices, and the Curve pool lost 5.1M USDC because it had no such delay. Builders who publish an exchange rate for other protocols to consume should treat it as an oracle, with the guards an oracle needs, and test it under the conditions a flash loan creates rather than the ones an operator expects. To see how Olympix traces your own rates from every source they read to every pool that trusts them, and returns a runnable counterexample where a single transaction can bend one, book a technical walkthrough.
On-Chain References
Protocol: Makina (DUSD Machine operated by Dialectic)
Chain: Ethereum
Date: January 20, 2026, 03:40:35 UTC
Loss: about $1,793,430 net (5,108,973 USDC drained from the DUSD/USDC Curve pool, minus 920.7 ETH, 157.1 ETH and 104,491 USDC returned)
Original attacker: 0x2F93...1CCc
Original attack contract deployment tx: 0xad94...5a39
MEV searcher (executed the exploit): 0x935b...de48
Exploit tx: 0x5697...51f5
Block: 24273362
DUSD/USDC Curve pool: 0x32E6...4cb3
DUSD Caliber: 0x0614...4898
DUSD token: 0x1e33...04ef
Block builder: 0xa6c2...8387
Builder address that returned funds: 0xbed2...dE25
Rocket Pool node distributor: 0x573D...910E
Builder to proposer transfer (276.32 ETH): 0xc244...dbb5
Builder to 0xbed transfer (1,023 ETH): 0x14d2...a2a7
Recovery Mode on DUSD Machine: 0x69c5...df45
Builder return (920.7 ETH): 0x0d23...6a44
Dialectic fee return (104,491 USDC): 0xc0e3...a4d9
Validator return (157.1 ETH): 0x4e25...4dd0
Recovery Safe: 0xc22F...8AB9
Sources
Makina: Post Mortem, January 20th 2026 Incident
Makina: Recovery and Restitution Strategy
CertiK: Makina Incident Analysis
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.




