October 20, 2026
|
Exploit Postmortem
Cover image for "Malt Protocol's $13.4K Loss and How Olympix Would Have Prevented It"

Malt Protocol's $13.4K Loss and How Olympix Would Have Prevented It

Date: October 3, 2026

Chain: Polygon

Loss: about $13.4K, 13,440.58 DAI per DeFiHackLabs' on-chain reconstruction (SlowMist's alert said about $72K in MALT, a figure the on-chain flows don't support)

Class: a swap invariant that counted treasury-funded liquidity as the caller's own input

On October 3, 2026, an attacker on Polygon called Malt Protocol's swap function three times with real inputs of 1 DAI, 1 DAI and then 50 DAI. In two of those rounds, the protocol's own treasury, which Malt calls the Capital Source, sent 6,286.10 DAI and then 6,458.54 DAI into the pool in the middle of the swap, and the swap credited that DAI to the attacker.

The attacker started with a 1,300 DAI flash loan from Balancer and finished with 13,440.58 DAI in net profit, a figure the DeFiHackLabs reproduction matches to the wei. The swap was possible because it checked its pricing rule against the pool's final balances, without separating the DAI the caller sent from the DAI the treasury had just added.

A Treasury That Defends the Peg

MALT is a stablecoin that defends its target price with protocol-owned capital. When MALT trades below peg in its MALT/DAI pool, the Capital Source treasury supplies DAI to the pool to push the price back up, and that rebalancing runs as an automatic side effect of the pool's own swap function, through an external call named rebalanceHook. On October 3, that mechanism supplied the DAI that the attacker's swaps were credited with.

Whose DAI Is It

According to SlowMist's alert, swap(uint256,uint256,address) records the caller's input and the pool's reserves, then calls rebalanceHook before sending the requested output. The hook pulls DAI from the Capital Source and deposits it into the same pool. The swap then validates its constant-product invariant, the rule that the product of the pool's two reserves can't go down after a trade, against the pool's final balances. Those balances include the treasury's DAI, so the check treats it as if the caller had supplied it. The DeFiHackLabs trace shows the same path from the other side: the pool's swap() calls into Malt's RewardThrottle contract, which has the Capital Source transfer DAI straight into the pool, all before the output leaves. Neither the pool nor the treasury has verified source on Polygonscan, so that mechanism was reconstructed from the call tree and then reproduced against the live contracts.

An invariant check is only as good as the inputs it's measured against. Picture a cashier who counts the till after a customer pays to decide how much change to hand back, while a manager quietly tops up the till from the safe in the same moment. The count looks generous, the customer gets change for money they never handed over, and the till balances perfectly on paper. Malt's swap needed to measure only what the caller paid, and it measured everything that arrived.

Three Rounds in One Transaction

The attack ran in a single transaction, 0x915e...445b in block 94869210, sent from the wallet 0x8F10...83Ef through an attack contract. The attacker borrowed 1,300 DAI from Balancer, bought MALT on QuickSwap, and sold it into the MALT/DAI pool to push the price below peg, the condition that switches on the Capital Source. Then the attacker called swap() with a negligible real input.

Below peg, the swap called rebalanceHook, the Capital Source deposited DAI into the pool, and the invariant check counted that DAI as the attacker's payment. The pool released far more MALT than 1 DAI could ever buy, and the attacker sold that MALT back on QuickSwap. The cycle ran three times, with inputs of 1, 1 and 50 DAI. The treasury paid in during two of the rounds, 6,286.10 DAI and 6,458.54 DAI, a total of 12,744.64 DAI, and the third round's trigger condition wasn't met.

The attacker repaid the flash loan and kept 13,440.581562307921517594 DAI. Every call in the sequence was unprivileged, and the pool's own public swap function was the entire attack surface.

Two Different Numbers

SlowMist's alert put the loss at about $72,000 in MALT, and most news coverage repeated that figure, but it doesn't match the on-chain flows. The DeFiHackLabs author rebuilt the attack against on-chain state, matched the result to the wei, and found that the transaction supports 13,440.58 DAI in attacker profit, alongside a 12,744.64 DAI drain of the treasury, so this post uses about $13.4K as the loss. No statement from the Malt team appears in the sources found.

How Olympix Separates the Caller's Input From Everything Else

Olympix reads a contract the way an attacker does, as states, transitions and entry points, and it takes in the context around the code, here a pool whose swap can trigger a treasury rebalance, to infer the invariants the system relies on. In Malt's pool, the states that matter are the reserves and the Capital Source balance, and swap() is the entry point that can move both in a single call.

Olympix's static analysis surfaces the signal: an external call that changes the pool's balances sitting between the snapshot of the caller's input and the invariant check, with nothing in the function separating caller-funded tokens from hook-funded ones. Olympix has flagged this class before in other codebases, where a pool's own push of tokens was counted toward a price check. The signal narrows the search from every path in the pool to the feasible ones below peg, where the hook actually fires.

BugPoCer works from the pool's accounting outward. It runs Counterexample-Guided Invariant Refutation over the pool, the rebalance path and the Capital Source together, and infers the rule the swap check silently depends on: every DAI the swap counts as input has to come from the caller. It then executes paths through swap() under different pool prices until one, below peg, lets the treasury's DAI pay for the caller's output.

The counterexample comes back as the October 3 attack: push the pool below peg, call swap() with 1 DAI, let rebalanceHook deposit treasury DAI, and watch the invariant check pay out MALT against DAI the caller never sent. BugPoCer returns that sequence with the minimal conditions that trigger it and a generated Forge test that replays it, produced in development instead of on mainnet. Because the engine runs automatically during development, at every merge and before release, a peg-defense hook wired into the swap path gets tested against the swap's own accounting as soon as it's added.

When the Protocol Is Also a Counterparty

Malt's treasury and its swap function each did what they were built to do, and the loss came from running one inside the other without telling the invariant check whose money was whose. Any protocol that injects its own capital during a user's transaction has to measure the user's contribution separately. To see the counterexamples Olympix produces when your protocol's own mechanisms run inside a user's call, book a technical walkthrough.

On-Chain References

Protocol: Malt Protocol, MALT/DAI pool and Capital Source

Chain: Polygon

Date: October 3, 2026

Loss: about $13.4K: 13,440.581562307921517594 DAI attacker profit and 12,744.64 DAI drained from the Capital Source, per DeFiHackLabs' on-chain reconstruction (SlowMist's early alert said about $72K in MALT)

Attacker EOA: 0x8F10...83Ef

Attack contract: 0x0DbF...0393

MALT/DAI pool (vulnerable contract): 0xfe6C...7A13

Capital Source (victim treasury): 0xF0d3...2AAC

MALT token: 0x16c8...50d4

QuickSwap MALT/DAI pair: 0xf864...3ce1

Flash loan: Balancer, 1,300 DAI

Exploit tx: 0x915e...445b

Block: 94869210

Sources

SlowMist: TI Alert on the MALT exploit

DeFiHackLabs PR #1289: MALT exploit PoC

DeFiHackLabs: MALT_exp.sol

KuCoin News: SlowMist TI Alert relay with addresses

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.