Aperture Finance's $3.7M Loss and How Olympix Would Have Prevented It
Date: January 25, 2026
Chain: Ethereum, Base, Arbitrum
Loss: about $3.67M in ERC-20 tokens and Uniswap V3 liquidity positions
Class: arbitrary external call built from caller-supplied target and calldata
At 17:10:35 UTC on January 25, 2026, one Aperture Finance user's wallet gave up 36.92 WBTC in a transaction that wasn't theirs. Before that day the user had done what Aperture's instant liquidity product asks of everyone, approved Aperture's liquidity contract to move their tokens so it could build Uniswap positions on their behalf, and on that Sunday an attack contract created inside the transaction itself used that standing approval to move the WBTC straight to its owner.
The same pattern repeated across Ethereum, Base and Arbitrum until about $3.67M in tokens and Uniswap V3 position NFTs had left the wallets of people who trusted Aperture's contracts. The cause was a swap step inside Aperture's V3/V4 liquidity contracts that made a low-level call to whatever address the caller named, with whatever calldata the caller wrote, from a contract that held its users' token approvals.
A Helper That Swaps Before It Mints
Aperture Finance runs an intents platform for concentrated liquidity. Revoke.cash's incident summary describes it as a service that manages users' positions on Uniswap, PancakeSwap, Aerodrome and Velodrome, and the product most users touched was what Aperture calls instant liquidity: minting a position, adding to one, or reinvesting fees in a single click. Building a Uniswap V3 position usually takes two tokens in a ratio set by the price range, so a one-click mint that starts from a single asset has to swap part of it first and then mint.
That's the job of the contract at 0xD83d...8913 on Ethereum. Users approve it to move their tokens and, per Revoke.cash, their LP position NFTs, and from then on the contract pulls what it needs, routes a swap through a DEX aggregator, and calls Uniswap's NonfungiblePositionManager to mint the position for the user. According to BlockSec's analysis, the mint entry point for native-token deposits is a function with the selector 0x67b34120, and its intended flow has three steps: wrap the ETH into WETH, swap through an internal routine BlockSec labels 0x1d33(), and mint the position.
The contract's source was never published. Etherscan lists it as unverified, and both BlockSec and SolidityScan had to decompile the bytecode and read execution traces to reconstruct what it does. Aperture later called the vulnerable piece a helper module, and according to Revoke.cash's summary of the team's thread, said its automation products (auto-rebalancing, auto-compounding and limit orders) run on separate contracts that weren't affected.
The Call That Went Wherever It Was Told
The swap step is where an aggregator integration has to give up some control. A DEX aggregator returns a router address and a blob of calldata describing the best route, and the integrating contract passes them along with a low-level call. Aperture's contract accepted three values for that step from the caller: the address to call, the calldata to send it, and the amount of output the swap was expected to produce.
BlockSec found that the contract checked the token approval it set up for the swap, which in normal transactions went to OKX DEX's TokenApprove contract, but didn't check the address it actually called. In a normal mint the call target is OKX's DexRouter. Nothing stopped a caller from naming a token contract as the target instead, and writing calldata for transferFrom(victim, attacker, amount). Because the low-level call ran from Aperture's contract, the token saw Aperture as the spender, found the victim's allowance in place, and moved the funds. Uniswap V3 position NFTs approved to the same contract could be taken the same way.
The contract did have a check after the swap. It compared the change in its own token balance against the expected output, which is a sensible guard for a real swap. SolidityScan points out why it couldn't help here: the expected output was one of the caller's own inputs, so a caller who set it to match a zero-output "swap" passed the check every time. The transferFrom never touched Aperture's balance at all, since the tokens went from the victim's wallet to the attacker's.
The single idea to take from this one is that an approval is a standing permission held by the contract, and any code path that lets an outside caller choose what the contract calls hands that permission to the outside caller. A valet who'll drive your car anywhere a stranger writes on a slip of paper hasn't been given your keys in name, but the stranger has them in practice.
One Deployment, One Transfer
SolidityScan and BlockSec both walk through Ethereum transaction 0x8f28...f25a as the representative case. Blockscout dates it to 17:10:35 UTC on January 25, 2026, in block 24313234, sent by 0xe3E7...e8eA. The transaction deployed an attack contract at 0x5c92...8130 and ran the whole sequence from its constructor.
The attack contract called Aperture's 0x67b34120 mint with 100 wei of ETH attached. Aperture wrapped the 100 wei into WETH as usual. Then its swap routine made the low-level call it had been handed, which in this transaction targeted the WBTC contract with a transferFrom from user wallet 0x5240...1f9d to the attacker's wallet. Blockscout's token transfer log for the transaction shows exactly that movement: 36.91897652 WBTC from 0x5240...1f9d to 0xe3E7...e8eA. The balance check passed because the expected output matched, and Aperture finished the flow by minting a Uniswap V3 position, token ID 1181114, to the attack contract using the 100 wei of WETH.
From the outside the transaction looked like an ordinary mint that happened to move a user's WBTC on the way. Every individual step was a call Aperture's contract was designed to make, which is why the same template could be pointed at a different victim, token or position NFT simply by changing the call target and calldata.
Blockaid's exploit detection flagged the pattern about an hour after the representative transaction. Its community alert at 18:17 UTC said it had seen multiple exploit transactions against Aperture's contract and estimated about $4M drained so far. The final tally across Ethereum, Base and Arbitrum settled at about $3.67M in the analyses BlockSec, SolidityScan and PeckShield published.
Same Day, Different Protocol, Same Mistake
Aperture wasn't alone that day. BlockSec covered it together with SwapNet, a separate protocol hit on January 25 for about $13.41M on Base, BNB Chain and Arbitrum through a flaw of the same family: user-supplied inputs let an attacker replace an expected router or pool address with a token address, so a low-level call drained approved tokens. BlockSec put the combined loss at about $17M and noted that both victims ran closed-source contracts.
That pairing matters for how these bugs get through. Neither contract was a lending market or a bridge with an obvious pot of money inside it. Each held almost nothing of its own, and the value at risk sat in users' wallets behind approvals that the contracts needed to do their jobs. A review focused on what the contract holds can easily rate a router-style helper as low-risk, while the approvals it accumulates make it one of the most valuable addresses a user ever interacts with.
Revoking, Tracing and Waiting
Aperture's first public word came at 23:19 UTC on January 25, in an X post headed "URGENT: Security Incident Notice". The team said it was aware of an exploit affecting the Aperture V3/V4 contract, had stopped core features on its frontend so no new approvals could be granted, and was investigating with security partners, and it asked users to revoke all approvals to the affected contracts. A follow-up thread titled "Security Incident Analysis, Jan 25, 2026" said the exploit had hit a vulnerability in a helper module and had been fully contained. Revoke.cash, summarizing that thread, reports that Aperture said only users who'd approved the instant liquidity contracts were at risk, that it was working with forensic firms and law enforcement, and that it would seek a negotiated return.
The money moved quickly. According to PeckShield, as reported by Coinpedia, the attacker converted a large share of the haul into ETH and sent about 1,242 ETH into Tornado Cash in batches of 10 ETH and 100 ETH. The Ethereum wallet behind the representative transaction kept working the stolen assets for days: Blockscout shows it running dozens of decreaseLiquidity and collect calls against Uniswap's V3 position manager on January 30, followed by a run of swaps into ETH through Uniswap's V2 router. AMLBot's on-chain breakdown reported that Aperture had opened on-chain communication with the attacker.
As of October 23, 2026, Aperture's most recent post on X was the incident analysis thread from 00:03 UTC on January 27, and no recovery or compensation announcement had followed it there.
Why the Bug Stayed Invisible
Closed source shaped this incident from both ends. Outside researchers couldn't read the helper module until it was being drained, and the first public explanations had to be rebuilt from bytecode and traces. Users granting approvals had no way to check what the contract would do with them beyond trusting the team's own testing and whatever review the code received.
Audits are necessary, and they cover the scope they're given in the time they have. A helper that wraps an aggregator call is often treated as glue code around audited pieces, the aggregator on one side and Uniswap's position manager on the other, and the dangerous behavior lives in the seam between them: the contract trusted the aggregator's calldata format without pinning down where that calldata was allowed to go. BlockSec's own reading of the two transactions makes the point plainly, since a normal mint and the malicious one approved the same spender and differed only in the address the contract called.
How Olympix Tests Where a Swap Call Actually Lands
Olympix reads a contract as an attacker would, as states, transitions and entry points, and it works on the source a team writes, so a helper that never gets published on a block explorer is just as readable to it during development. For this module the state that matters is the set of allowances users have granted it, and the entry points that matter are every external function that ends in a low-level call. The engine maps those paths and uses signals to choose which ones to attack. The signal here is plain in the code: an external call whose target address and calldata both flow from function parameters, made by a contract that holds third-party ERC-20 and ERC-721 approvals.
From that signal the path narrows quickly. Of all the paths into the mint function, the feasible ones are those an unprivileged caller can reach with a small amount of ETH, and the ones that touch the invariant are those where the caller's target and calldata reach the call unchanged. The invariant fits in one sentence: the contract should only ever move a user's tokens on behalf of that same user, and only as part of the swap and mint that user asked for.
BugPoCer is the method that turns that rule into a result. It runs Counterexample-Guided Invariant Refutation, working outward from the system as a whole: it infers the invariant from how the contract is meant to use approvals, then executes paths until one breaks it. Here the breaking path is short: an address with no approvals of its own reaches the native-token mint and steers the swap call into a transfer drawn on another user's allowance, while the post-swap balance check is satisfied by the caller's own expected-output value. The output is a counterexample with the sequence of calls and balance changes, the minimal conditions that make it work (a victim with a live allowance and a caller-chosen call target), and a generated Forge test that replays it. That counterexample is the January 25 transaction, produced on a developer's machine before the helper ever held an approval.
Olympix has flagged this class before in other codebases, where untrusted input decided whose tokens a contract would move. Because the engine runs automatically in development, at every merge and before release, a change that added or loosened the swap step's target handling would have raised the finding at that merge, alongside a test showing exactly which allowance it could spend.
A Contract Is Only as Narrow as Its Calls
Aperture's helper held no balance worth stealing, and that made it easy to underestimate. What it held was permission, from every user who'd ever clicked mint, and one unpinned call target turned that permission into anyone's to use. For builders integrating aggregators, the rule to encode is that a contract holding approvals decides where its calls go, every time, and never lets the caller decide. If your contracts route user funds through calls built from outside data, book a technical walkthrough.
On-Chain References
Protocol: Aperture Finance (V3/V4 instant liquidity contracts)
Chain: Ethereum, Base, Arbitrum
Date: January 25, 2026 (representative Ethereum transaction at 17:10:35 UTC)
Loss: about $3.67M across chains (BlockSec, SolidityScan, PeckShield)
Attacker EOA (Ethereum): 0xe3E7...e8eA
Attack contract (Ethereum): 0x5c92...8130
Victim contract (Ethereum, source unverified): 0xD83d...8913
Drained user wallet in representative tx: 0x5240...1f9d
Representative exploit tx: 0x8f28...f25a
Block: 24313234
Normal mint tx for comparison: 0xc823...0200
Sources
BlockSec: $17M Closed-Source Smart Contract Exploit, Arbitrary Call in SwapNet and Aperture
SolidityScan: Aperture Finance Hack Analysis
Revoke.cash: Aperture Finance Hack
Aperture Finance on X: Security Incident Analysis, Jan 25, 2026
Blockaid on X: Community Alert on Aperture Finance
AMLBot: $13.5M Lost in Aperture Finance and SwapNet Exploit, Full On-Chain Breakdown
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.




