Fence Finance's $1.01M Loss and How Olympix Would Have Prevented It
At 23:56:41 UTC on October 27, 2026, a contract that had existed for fifteen minutes borrowed 1,013,353.61 USDC from Aave V3 on Avalanche, ran it through a lending pool tied to Fence Finance, and handed it straight back to Aave with about $507 in fees. Nothing left the pool that night. The attacker had only planted a promise to repay that money at midnight, a promise they never meant to keep. Three minutes and twenty-two seconds later, at 00:00:03 UTC on October 28, the same attacker called the pool again, and the pool sent out 1,013,353.61 USDC that belonged to someone else's lenders.
That second transaction is the exploit. The pool's accounting treated a repayment that had merely come due as a repayment that had actually arrived, and the moment the UTC date changed, the attacker's borrowing limit reopened in full. Every tranche in the pool drew on one shared USDC balance, so the money that came out was the idle cash of tranche #0, funded by real lenders. By 00:05:22 UTC, the attacker had moved the entire haul off Avalanche through Circle's cross-chain transfer protocol. By 03:16 UTC, Fence was writing to the attacker on-chain, offering a 10% bounty for the return of the rest.
A Private Credit Desk Running on Avalanche
Fence builds software for asset-backed finance, the corner of credit markets where lenders fund pools of receivables, loans or other assets and a stack of agents checks the collateral, calculates what's owed and moves the cash. Fence's pitch is to replace those agents with smart contracts that verify assets, calculate borrowing bases, enforce covenants and move funds in real time. According to Fintech Global, the company raised a $20 million Series A in April 2026 led by Galaxy Ventures and reported $1.5 billion in assets under administration across active facilities.
The pool at the center of this incident, contract 0xe25c...621a, held USDC committed to a credit facility on Avalanche's C-Chain. SlowMist described the pool as associated with Valinor Digital, OatFi and Fence, and the public reports so far don't say how responsibility for the contract is split between them. Fence is the party that has spoken. The recovery message sent to the attacker after the drain was written in Fence's name and pointed to a fence.finance address.
The contract's source code was never verified on any Avalanche explorer, so nobody outside the team can quote the exact functions. What the community knows about its design comes from reading the transactions themselves, and the clearest account of that comes from Defimon, the automated monitor run by Decurity, which traced the call flow within hours.
How the Tranches Shared One Wallet
According to Defimon, the pool ran several lending tranches inside a single contract. In each tranche, lenders deposit USDC and a designated borrower draws against those deposits on a repayment schedule. Instead of storing each tranche's settings in full, the pool asked the caller to pass in the tranche's configuration on every call (the borrower and admin, a hook contract, the asset and the schedule parameters) and checked that bundle against a stored hash.
Two design choices sat on top of that. The first was who could open a tranche. Creating one only required the caller to be the admin named in the configuration the caller supplied, which meant anyone could open a tranche and name themselves admin, borrower and lender all at once. The hook contract, meant to let a tranche plug in custom checks, could also be the caller's own contract, so every hook check could simply return true.
The second choice was where the money lived. Each tranche kept its own books, but every tranche paid out of the same USDC balance held by the pool contract. Tranche #0 was the real facility. Per Defimon, its lenders had deposited 3 million USDC and its borrower had drawn about 1.986 million, which left roughly 1.013 million USDC sitting idle in the contract. A new tranche created by a stranger would draw from that same balance.
Neither choice is fatal on its own. A shared balance works when each tranche's books keep its draws within its own deposits. Permissionless tranche creation is harmless when a new tranche can only ever take out what its own lenders put in. The pool's safety rested entirely on the arithmetic that tied each tranche's draws to its own money, and that arithmetic had a hole at midnight.
The Midnight Rule That Booked an IOU as Cash
The draw function capped how much a borrower could take at deposits minus outstanding debt. Outstanding debt went down when principal was repaid. The flaw was in how the pool decided that principal had been repaid.
SlowMist's root cause is direct: at the UTC day rollover, the pool's settlement logic treated a scheduled principal payment as received, reducing recorded outstanding principal even though no USDC had been repaid. Defimon's reading of the trace adds the detail that makes it exploitable. When a repayment was due on the current day, the pool booked it as paid and took it off the tranche's outstanding debt. The draw cap then read that reduced debt and reopened the borrowing limit by the same amount.
The pool did know how to treat unpaid principal carefully elsewhere. According to Defimon, the lender-withdrawal check subtracts principal that fell due and went unpaid. The borrower's draw check doesn't. Two code paths asked the same question, how much does this tranche really owe, and came back with different answers. The withdrawal path said the debt was still there. The draw path said it had been settled.
In a traditional facility, a paying agent confirms that cash landed before anyone updates the ledger, and a missed payment triggers a default process. Fence's contract folded both jobs into one piece of code, and the piece that moved the ledger never looked at the cash. A repayment schedule is a calendar entry. The pool treated it as a receipt.
The Attack, Transaction by Transaction
The attacker's wallet, 0x6ff1...1246, was funded with AVAX a little over an hour before the attack. At 23:41:49 UTC it deployed the attack contract at 0xf09d...7805. At 23:56:39 UTC, two seconds before the setup, the wallet sent 506.676807 USDC to that contract, one unit more than the flash loan premium it was about to pay.
The setup transaction landed at 23:56:41 UTC. Inside it, the attack contract took a flash loan of 1,013,353.611686 USDC through Aave V3, a figure that matched tranche #0's idle cash to the cent. The contract then opened tranche #1 with itself as admin and as hook, deposited the full loan into tranche #1 as its lender, and, acting as admin, scheduled a principal repayment of 1,013,353.61 USDC due at 00:00 UTC on October 28. Acting as borrower, it drew the whole deposit back out. The on-chain transfers show the USDC moving from Aave's aUSDC reserve to the attack contract, into the pool, back out of the pool to the attack contract, and finally 1,013,860.288492 USDC back to Aave, which covered the loan plus the 506.676806 USDC premium.
When that transaction ended, the pool's balance was exactly where it had started. What had changed was the paperwork. Tranche #1 now owed 1,013,353.61 USDC in principal, all of it scheduled to come due at the next UTC midnight, and the attacker had no intention of paying any of it.
The exploit transaction landed at 00:00:03 UTC, three seconds into the new day. The attacker's wallet called its contract, which called the pool's draw function with the same parameters as before. SlowMist's alert notes that the attacker reused identical calldata for the second draw. This time, the pool saw a repayment due today, booked it as paid, cut tranche #1's outstanding debt to zero and recalculated the draw cap at the full 1,013,353.61 USDC. The pool transferred 1,013,353.611686 USDC to the attack contract, and the contract forwarded 1,013,353.611687 USDC to the attacker's wallet. The pool's USDC balance fell to zero.
No oracle was manipulated and no price moved. The flash loan in the setup step only gave the attacker a large enough deposit to schedule a repayment that matched tranche #0's idle cash. The drain itself needed no borrowed capital at all.
Five Minutes to Solana
The attacker didn't wait. At 00:03:41 UTC, the wallet burned 250,000.175563 USDC through Circle's Cross-Chain Transfer Protocol, using the depositForBurnWithHook function with Solana as the destination domain. At 00:05:22 UTC, it burned another 763,374.836640 USDC on the same route to the same Solana recipient. Together that's 1,013,375.012203 USDC, the stolen funds plus a small amount left over from the wallet's earlier swaps. As of October 28, 2026, the attacker's Avalanche wallet held no USDC.
Detection ran in parallel. Unchained reported that Blockaid flagged the exploit as ongoing and first described it as flash-loan-funded manipulation of tranche shares. SlowMist's follow-up pinned the cause on the repayment check, and Defimon's breakdown laid out the full sequence from the setup to the second draw.
At 03:16:23 UTC on October 28, the address 0x3759...00a2 sent the attacker's wallet an on-chain message on Fence's behalf. It asked the controller of the wallet to resolve the incident cooperatively, offered a 10% recovery bounty with 90% of the affected funds returned, gave recovery@fence.finance as a contact and asked the attacker to keep the funds in place. As of October 28, 2026, no return of funds had been reported, and Fence, Valinor Digital and OatFi hadn't published a post-mortem.
Why the Gap Survived Until Midnight
The bug lives in a corner that's easy to underweight. Normal operation of a credit facility is a sequence of deposits, draws and repayments that arrive on time. Test suites and reviews naturally follow those paths: a lender deposits, a borrower draws, the borrower repays, the cap moves accordingly. The failure here needed three conditions that rarely appear together in a happy-path test. A repayment had to fall due and go unpaid. The borrower had to draw again on the day it fell due. The tranche drawing had to be a different one from the tranche whose cash was idle.
Real-world asset protocols import a lot of machinery from traditional finance, including day counts, due dates and settlement windows, and every one of those time-based rules is a new state transition that can fire without anyone sending money. Audits are a necessary part of shipping a contract like this, and their value depends on the scope and the scenarios they're asked to cover. A scenario where an attacker creates their own tranche four minutes before midnight and schedules their own default sits well outside the paths a reviewer is usually pointed at. It's exactly the kind of path a machine search for broken invariants is built to walk.
How Olympix Finds the Debt That Disappears at Midnight
Olympix reads a contract as a system of states, transitions and entry points. It takes in the surrounding context, such as documentation, design notes and the way the code itself is written, to infer the invariants the system depends on but never states outright. For the TranchePool, the states that matter are each tranche's deposits, its outstanding principal and its repayment schedule, plus the single USDC balance every tranche draws from. The transitions are deposit, draw, repay and the day rollover that settles scheduled principal. The entry points are the public functions, including tranche creation.
From there, Olympix maps the paths through the contract and uses signals to narrow them, first from every path to the feasible ones, then to the feasible paths that touch an invariant. Static analysis supplies those signals here. It surfaces the settlement step that lowers outstanding principal based on a date comparison, with no matching transfer of USDC into the pool on that path. It also surfaces the mismatch between the two places that read outstanding debt: the lender-withdrawal check subtracts principal that fell due unpaid, and the borrower's draw cap doesn't. Two readers of the same quantity disagreeing about its value is a strong signal that state is drifting out of sync. Olympix has flagged this class of bug before in other codebases, where an earnings or debt figure tracked in one place stopped matching the same figure tracked in another.
The invariant that matters can be written in one sentence. A tranche can never draw more than its own lenders deposited minus the principal it hasn't actually repaid, and the sum of all draws can never take another tranche's lenders' money. BugPoCer, Olympix's tool for Counterexample-Guided Invariant Refutation, starts from the system and works outward. It takes that invariant, then searches for a sequence of calls that breaks it, refining the search each time a candidate fails.
The sequence it lands on mirrors the attack. Open a new tranche with the same address as admin, borrower and hook. Deposit an amount as lender. Schedule a principal repayment for the next day. Draw the deposit back out. Advance the block timestamp past the UTC rollover. Call draw again with the same parameters. On the second draw, the tranche's recorded debt has dropped to zero with no USDC received, the cap has reopened, and the transfer comes out of the shared balance that belongs to another tranche's lenders. The invariant breaks.
What BugPoCer returns is a counterexample: the exact sequence of state changes, the minimal conditions that trigger it (an unpaid repayment due on the current day, a second draw after the rollover and idle cash in another tranche at least as large as the scheduled amount) and a generated Forge test that replays the whole thing against the contract. That test is the October 28 drain, run on a developer's machine at merge time with no lenders' money in the pool. Like any counterexample, it shows one concrete way the invariant breaks. It doesn't certify that no other way exists, which is why the search runs again at every merge and before every release.
The permissionless tranche creation shows up in that same counterexample as the step that makes the attack cheap. Without it, an attacker would need a legitimate borrower's role to schedule a repayment they could skip. Because the counterexample includes tranche creation as its first step, the team sees both changes the fix needs: settle principal only on a confirmed transfer, and make the draw cap subtract unpaid due principal the same way the withdrawal check already does.
Repayment Is a Transfer
Fence's pool did most things the way a credit system should. It separated lenders by tranche, tracked principal and enforced a draw cap. The one rule it got wrong was treating the calendar as proof of payment, and an attacker needed only a flash loan, a self-made tranche and three seconds past midnight to turn that rule into a million dollars. For anyone building on-chain credit, the lesson is concrete. Any value that gates a draw or a withdrawal should only fall when tokens actually arrive, and every function that reads it should read it the same way. To see how Olympix maps those accounting paths in your own contracts and hands back a runnable counterexample for every cap that can be reopened, book a technical walkthrough.
On-Chain References
Exploit transaction (Oct 28, 00:00:03 UTC): 0x3f6b...8c77
Setup transaction (Oct 27, 23:56:41 UTC): 0x19ab...99fd
Victim pool (unverified source): 0xe25c...621a
Attacker wallet: 0x6ff1...1246
Attack contract: 0xf09d...7805
CCTP burn to Solana, 250,000.18 USDC: 0x0e97...218b
CCTP burn to Solana, 763,374.84 USDC: 0x51bb...f397
Fence bounty message to the attacker: 0xaf19...4894
Sources
Blockfence: Avalanche Tranche Vault Exploited for $1.01 Million in USDC
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.




