<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: PandaSecurity</title>
    <description>The latest articles on DEV Community by PandaSecurity (@pandasecurity).</description>
    <link>https://dev.to/pandasecurity</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F915214%2Fcfc5c970-b102-4be4-9ab2-9c71b0d38065.jpg</url>
      <title>DEV Community: PandaSecurity</title>
      <link>https://dev.to/pandasecurity</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pandasecurity"/>
    <language>en</language>
    <item>
      <title>Loopscale: Pricing Misconfig and NAV Loan Exploit</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Fri, 06 Nov 2026 23:13:44 +0000</pubDate>
      <link>https://dev.to/pandasecurity/loopscale-pricing-misconfig-and-nav-loan-exploit-1eok</link>
      <guid>https://dev.to/pandasecurity/loopscale-pricing-misconfig-and-nav-loan-exploit-1eok</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/loopscale-genesis-vault-pricing-nav-exploit/" rel="noopener noreferrer"&gt;Loopscale: Pricing Misconfig and NAV Loan Exploit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;5 November 2026&lt;/strong&gt;, &lt;strong&gt;Loopscale&lt;/strong&gt; — a Solana order-book lending protocol built around fixed-rate, overcollateralized credit markets and &lt;strong&gt;Genesis Vaults&lt;/strong&gt; — paused operations after a week-long campaign of malicious API traffic. The next day the team published a two-path disclosure: a &lt;strong&gt;SOL Genesis Vault&lt;/strong&gt; pricing misconfiguration that enabled an &lt;strong&gt;undercollateralized loan of approximately $412K&lt;/strong&gt;, and a &lt;strong&gt;USDC Genesis Vault&lt;/strong&gt; NAV-inflation attempt that sought a &lt;strong&gt;disproportionate redemption of approximately $521K&lt;/strong&gt;, which Loopscale says &lt;strong&gt;their systems halted within 30 seconds of transaction generation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post summarizes only what Loopscale and corroborating public reports disclosed. We did not find published Solana transaction signatures for either path in those sources, so the analysis follows the team's disclosed paths rather than independently reconstructed receipts. Treat any illustrative logic as a &lt;strong&gt;simplified illustration&lt;/strong&gt;, not verified Loopscale program source — the team did not publish program snippets in the posts we cite.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; Loopscale Genesis Vaults on &lt;strong&gt;Solana&lt;/strong&gt; (SOL and USDC vaults called out).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window:&lt;/strong&gt; Week-long campaign of more than &lt;strong&gt;30,000 malicious API requests per day&lt;/strong&gt;, culminating in a pause on &lt;strong&gt;5 Nov 2026&lt;/strong&gt;; detailed two-path update on &lt;strong&gt;6 Nov 2026&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path 1 — SOL Genesis Vault pricing misconfiguration (~$412K):&lt;/strong&gt; a misconfigured combination of asset-pricing parameters valued deposited collateral as a different asset, enabling an undercollateralized loan of about &lt;strong&gt;$412K&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path 2 — USDC Genesis Vault NAV inflation (~$521K halted):&lt;/strong&gt; an atomic transaction bundle sold a loan carrying an extreme interest rate into the USDC Genesis Vault, inflating its NAV and enabling a disproportionate redemption of about &lt;strong&gt;$521K&lt;/strong&gt;, which Loopscale says was &lt;strong&gt;halted within 30 seconds&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User impact (per Loopscale):&lt;/strong&gt; affected vaults have been &lt;strong&gt;made whole&lt;/strong&gt;; &lt;strong&gt;no user deposits have incurred a loss&lt;/strong&gt;, and &lt;strong&gt;no user deposits will incur a loss&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restoration status (6 Nov):&lt;/strong&gt; priority lending restored; collateral top-ups for all loans; repayments for all except USDC Genesis Vault-funded loans; collateral withdraw after full repay OK except for USDC Genesis Vault-funded loans; vault withdrawals still restricted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-chain Solana signatures&lt;/strong&gt; for these paths were &lt;strong&gt;not published&lt;/strong&gt; in the primary sources below — do not invent them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Loopscale Disclosed
&lt;/h2&gt;

&lt;p&gt;Loopscale's pause announcement on &lt;strong&gt;5 November 2026&lt;/strong&gt; framed the event as the end of a week of coordinated probing. Protocol APIs received &lt;strong&gt;more than 30,000 malicious requests per day&lt;/strong&gt;. Attack attempts targeted the &lt;strong&gt;SOL and USDC Genesis Vaults&lt;/strong&gt;. Both paths involved &lt;strong&gt;sophisticated, multi-step transactions&lt;/strong&gt; aimed at &lt;strong&gt;publicly inspectable program logic&lt;/strong&gt; and &lt;strong&gt;private transaction-building APIs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;6 November&lt;/strong&gt; update separated the economic story into two failure modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Vault&lt;/th&gt;
&lt;th&gt;Mechanism (per Loopscale)&lt;/th&gt;
&lt;th&gt;Figure&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;SOL Genesis Vault&lt;/td&gt;
&lt;td&gt;Misconfigured asset-pricing parameters valued collateral as a different asset&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;$412K&lt;/strong&gt; undercollateralized loan&lt;/td&gt;
&lt;td&gt;Vault made whole; no depositor loss per team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;USDC Genesis Vault&lt;/td&gt;
&lt;td&gt;Atomic bundle sold an extreme-rate loan into the vault, inflating NAV&lt;/td&gt;
&lt;td&gt;~&lt;strong&gt;$521K&lt;/strong&gt; disproportionate redemption&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Halted / intercepted within ~30 seconds&lt;/strong&gt;; vault made whole; no depositor loss per team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total exposure framing: roughly &lt;strong&gt;$412K&lt;/strong&gt; of undercollateralized borrowing plus a &lt;strong&gt;~$521K&lt;/strong&gt; attempted redemption that was &lt;strong&gt;stopped&lt;/strong&gt; — not a permanent ~$933K depositor loss. Loopscale's public position is that vaults were made whole and depositors did not — and will not — take a loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 1 — SOL Genesis Vault Pricing Misconfiguration
&lt;/h2&gt;

&lt;p&gt;Order-book lending and vault products live or die on whether collateral is priced as the asset the depositor actually posted. Loopscale says an attacker exploited a &lt;strong&gt;misconfigured combination of asset-pricing parameters&lt;/strong&gt; so that collateral deposited in the &lt;strong&gt;SOL Genesis Vault&lt;/strong&gt; was &lt;strong&gt;valued as a different asset&lt;/strong&gt;. With that wrong valuation in place, the protocol allowed an &lt;strong&gt;undercollateralized loan of approximately $412K&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is a configuration / pricing-parameter failure, not a claim that Solana consensus broke. The dangerous property is familiar from EVM vault incidents: if an internal price table or asset-id mapping disagrees with the asset that actually moved, the protocol lends against fiction. Our &lt;a href="https://dev.to/blog/corevault-insurancepool-spot-price-oracle-exploit/"&gt;CoreVault InsurancePool spot-price exploit&lt;/a&gt; is a different chain and bug class, but it shares the lesson that &lt;strong&gt;any value-moving path that trusts a mis-set or attacker-movable price will under-collateralize itself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A simplified sketch of the &lt;em&gt;shape&lt;/em&gt; of a pricing-parameter mix-up — &lt;strong&gt;not&lt;/strong&gt; Loopscale source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Simplified illustration. Not Loopscale verified source.
# Collateral posted: SOL
# Pricing table (misconfigured): maps vault slot -&amp;gt; price feed for asset X != SOL

posted = deposit(SOL_amount)
value  = price_of(configured_asset_id) * posted   # wrong asset priced
loan   = borrow(value * LTV)                      # loan sized off fiction
# Result: undercollateralized exposure until parameters are corrected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without published program snippets or Solana signatures in the cited sources, we cannot assert function names or the exact parameter that was wrong. The verified claim stops at Loopscale's description: &lt;strong&gt;misconfigured pricing parameters → wrong asset valuation → ~$412K undercollateralized loan&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path 2 — USDC Genesis Vault NAV Inflation (Halted)
&lt;/h2&gt;

&lt;p&gt;The second path targeted vault accounting. Per Loopscale, the attacker used an &lt;strong&gt;atomic transaction bundle&lt;/strong&gt; to &lt;strong&gt;sell a loan carrying an extreme interest rate&lt;/strong&gt; into the &lt;strong&gt;USDC Genesis Vault&lt;/strong&gt;. That sale &lt;strong&gt;inflated the vault's NAV&lt;/strong&gt;, which enabled a &lt;strong&gt;disproportionate redemption of approximately $521K&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Critically, Loopscale states that &lt;strong&gt;their systems halted the redemption within 30 seconds of transaction generation&lt;/strong&gt;. The ~$521K figure is therefore an &lt;strong&gt;attempted / intercepted&lt;/strong&gt; redemption size, &lt;strong&gt;not&lt;/strong&gt; a confirmed permanent user loss. Reporting it as "stolen" would contradict the team's disclosure.&lt;/p&gt;

&lt;p&gt;Vault products that mark NAV from the loans they hold inherit every extreme parameter those loans can carry. An interest rate that is legal in the program but economically absurd can reprice share value if the vault treats the loan mark as authoritative. Atomic bundles on Solana make the inflate-and-redeem sequence hard to interrupt mid-flight unless monitoring and circuit breakers sit close to the transaction-building path — which appears to be what Loopscale credits for the 30-second halt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Simplified illustration. Not Loopscale verified source.
# 1) Sell a loan with an extreme interest rate into the USDC Genesis Vault
# 2) Vault marks the loan into NAV -&amp;gt; share price jumps
# 3) Redeem a disproportionate amount against the inflated NAV
# 4) Loopscale: halt / intercept within ~30s of tx generation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again: no public Solana transaction signatures for this path appeared in the primary sources we used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Containment, Addresses, and Restoration
&lt;/h2&gt;

&lt;p&gt;Loopscale linked these addresses in its pause disclosure (roles beyond "linked by Loopscale" were not further specified):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solana&lt;/td&gt;
&lt;td&gt;&lt;code&gt;247Lk5YNH9URfBBvxVUtDruz8GvGX8gKGuYHVCZThpiR&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AzPh2dUV3duK9n8PWV6CxKMwWr9Rk7fxPNBZuv1CiwMr&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EVM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xb8959Aca4e4C796A3BaDA66833eAe60b1BC0bfb2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EVM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xB7Ec7eF24FD73f3000C651CA9D0Fa45b61DcF854&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Containment claims (Loopscale):&lt;/strong&gt; affected vaults &lt;strong&gt;made whole&lt;/strong&gt;; &lt;strong&gt;no user deposits have incurred / will incur a loss&lt;/strong&gt;; priority lending restored on &lt;strong&gt;6 Nov&lt;/strong&gt;; collateral top-ups for all loans; repayments for all except USDC Genesis Vault-funded loans; collateral withdraw after full repay OK except USDC Genesis Vault-funded; vault withdrawals still &lt;strong&gt;restricted&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Brief Historical Context
&lt;/h2&gt;

&lt;p&gt;On &lt;strong&gt;26 April 2025&lt;/strong&gt;, an earlier Loopscale-related exploit drained roughly &lt;strong&gt;5.7M USDC&lt;/strong&gt; and &lt;strong&gt;~1,211.4 SOL&lt;/strong&gt; via RateX principal-token pricing manipulation; funds were later recovered and users took no net loss per contemporaneous reporting. That episode is background only — a different mechanism from the November 2026 Genesis Vault paths — but it underscores why pricing and NAV integrity remain the recurring theme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Builders and Auditors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Treat asset-id and price-feed configuration as security-critical code.&lt;/strong&gt; A wrong mapping that values SOL collateral as another asset is enough to underwrite an undercollateralized loan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bound what a vault may mark into NAV.&lt;/strong&gt; Extreme interest rates and atomic inflate-then-redeem bundles should fail closed or hit hard caps before share price moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put circuit breakers next to private transaction-building APIs.&lt;/strong&gt; Rate limits, anomaly detection on request volume (here: &lt;strong&gt;&amp;gt;30k malicious requests/day&lt;/strong&gt;), and automated halts on abnormal redemption size are what turned path 2 into a 30-second intercept.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assume multi-step, multi-chain follow-through.&lt;/strong&gt; Publishing both Solana and EVM addresses early helps responders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reuse a structured review checklist.&lt;/strong&gt; Pricing tables, vault NAV marking, and redemption paths are covered in our &lt;a href="https://dev.to/blog/smart-contract-security-checklist/"&gt;smart contract security checklist&lt;/a&gt;. Atomic funding patterns that amplify a bad mark are cousins of the patterns in our &lt;a href="https://dev.to/blog/flash-loan-attack/"&gt;flash loan attack&lt;/a&gt; overview, even on Solana.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Still Unknown
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Public &lt;strong&gt;Solana transaction signatures&lt;/strong&gt; for path 1 and path 2 were &lt;strong&gt;not&lt;/strong&gt; included in the Loopscale posts or secondary reports cited below.&lt;/li&gt;
&lt;li&gt;Loopscale did not publish program snippets in those posts; any code-shaped diagrams above are &lt;strong&gt;simplified illustrations&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Exact post-halt accounting (how vaults were made whole) was not detailed in the sources we used.&lt;/li&gt;
&lt;li&gt;Aggregators may list a single headline figure; prefer Loopscale's two-path split (~$412K undercollateralized loan + ~$521K halted redemption) over collapsing both into one "lost" number.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Addresses and Figures
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Protocol&lt;/td&gt;
&lt;td&gt;Loopscale (Solana order-book lending / Genesis Vaults)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chain&lt;/td&gt;
&lt;td&gt;Solana&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pause / two-path disclosure&lt;/td&gt;
&lt;td&gt;5 Nov 2026 / 6 Nov 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malicious API traffic&lt;/td&gt;
&lt;td&gt;&amp;gt;30,000 requests/day over the past week (Loopscale)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path 1&lt;/td&gt;
&lt;td&gt;SOL Genesis Vault pricing misconfig → ~$412K undercollateralized loan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path 2&lt;/td&gt;
&lt;td&gt;USDC Genesis Vault NAV inflation → ~$521K redemption &lt;strong&gt;halted within 30s&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User deposits&lt;/td&gt;
&lt;td&gt;Made whole; no loss incurred / will be incurred (Loopscale)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana (linked)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;247Lk5YNH9URfBBvxVUtDruz8GvGX8gKGuYHVCZThpiR&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solana (linked)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AzPh2dUV3duK9n8PWV6CxKMwWr9Rk7fxPNBZuv1CiwMr&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EVM (linked)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xb8959Aca4e4C796A3BaDA66833eAe60b1BC0bfb2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EVM (linked)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xB7Ec7eF24FD73f3000C651CA9D0Fa45b61DcF854&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prior incident (background)&lt;/td&gt;
&lt;td&gt;26 Apr 2025 — ~5.7M USDC + ~1,211.4 SOL via RateX PT pricing; funds recovered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://x.com/Loopscale/status/2118285556123611378" rel="noopener noreferrer"&gt;Loopscale pause announcement&lt;/a&gt; (5 Nov 2026): &amp;gt;30k malicious requests/day; vaults made whole; linked addresses; pause&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/Loopscale/status/2118502123705159769" rel="noopener noreferrer"&gt;Loopscale two-path update&lt;/a&gt; (6 Nov 2026): SOL pricing misconfig ~$412K; USDC NAV loan ~$521K halted in 30s; restoration status&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cryptobriefing.com/loopscale-pauses-operations-genesis-vaults-attack/" rel="noopener noreferrer"&gt;CryptoBriefing&lt;/a&gt;: narrative of the pause and Genesis Vault targeting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://crypto.news/loopscale-halts-operations-after-30000-daily-malicious-api-requests/" rel="noopener noreferrer"&gt;crypto.news&lt;/a&gt;: API-request volume and halt coverage&lt;/li&gt;
&lt;li&gt;ChainCatcher / Lookonchain / GetChain: corroborating ~$412K / ~$521K figures from official announcements&lt;/li&gt;
&lt;li&gt;DefiLlama hacks entry for Loopscale Lending, 5 Nov 2026&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Loopscale's November 2026 incident is a reminder that Genesis-style vaults inherit every assumption baked into pricing parameters and loan marking. One path under-collateralized a loan because collateral was valued as the wrong asset; the other inflated NAV by selling an extreme-rate loan into a vault and racing a redemption — a race Loopscale says it won inside thirty seconds. If you ship fixed-rate credit markets or vault products that mark NAV from loans, &lt;a href="https://dev.to/contact/"&gt;talk to our team&lt;/a&gt; or check our &lt;a href="https://dev.to/pricing/"&gt;audit pricing&lt;/a&gt; before those parameters and redemption paths go live against real deposits.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>security</category>
      <category>solana</category>
      <category>defi</category>
    </item>
    <item>
      <title>TENDRIL Exploit: Sell-Hook Burn-Sync Drained ~$555K</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Thu, 05 Nov 2026 23:07:30 +0000</pubDate>
      <link>https://dev.to/pandasecurity/tendril-exploit-sell-hook-burn-sync-drained-555k-c7o</link>
      <guid>https://dev.to/pandasecurity/tendril-exploit-sell-hook-burn-sync-drained-555k-c7o</guid>
      <description>&lt;p&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/tendril-sell-hook-burn-sync-usdt-drain/" rel="noopener noreferrer"&gt;TENDRIL Exploit: Sell-Hook Burn-Sync Drained ~$555K&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;16:02:45 UTC on 4 November 2026&lt;/strong&gt;, one transaction on &lt;strong&gt;BNB Smart Chain&lt;/strong&gt; emptied nearly all of the USDT sitting in the &lt;strong&gt;TENDRIL/USDT&lt;/strong&gt; PancakeSwap V2 pair. Public alerts from CertiK, BlockSec Phalcon, ExVul, ScaleBit and TenArmor put the pool's loss around &lt;strong&gt;$550K–$556K&lt;/strong&gt;. The same transaction repaid a &lt;strong&gt;~30.64M USDT&lt;/strong&gt; flash loan and sent &lt;strong&gt;308,959.37 USDT&lt;/strong&gt; to a profit address. There was no compromised admin key and no bug in PancakeSwap itself. The token's own sell-tax path burned TENDRIL out of the pair, called &lt;code&gt;sync()&lt;/code&gt; while the seller's remaining tokens were still in flight, and left the pool with a near-zero TENDRIL reserve that a follow-up swap could cash out as USDT.&lt;/p&gt;

&lt;p&gt;This is the same family of failure as other BNB Chain tax tokens that rewrite pair balances mid-transfer. The &lt;a href="https://dev.to/blog/nem-token-skim-burn-exploit-analysis/"&gt;NEM skim-and-burn exploit&lt;/a&gt; burned pair inventory through a faked sell counter. TENDRIL's version was simpler: every taxed sell asked a helper contract to burn from the pair and sync before the outer transfer finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; TENDRIL (&lt;code&gt;0xb1afdfd850da168f2cc9f742e11cdda5273973d2&lt;/code&gt;) and its PancakeSwap V2 USDT/TENDRIL pair (&lt;code&gt;0x5de082471f2a9f01a61e467a32837dee830d99e0&lt;/code&gt;) on BNB Chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploit transaction:&lt;/strong&gt; &lt;code&gt;0xfdff4fba4327821c204af07944076ecaaee163a994398fdf4063d08712e40059&lt;/code&gt;, block &lt;strong&gt;131,653,427&lt;/strong&gt;, &lt;strong&gt;4 Nov 2026, 16:02:45 UTC&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operator / caller:&lt;/strong&gt; &lt;code&gt;0xb81e4aafa347b27112b7f6f5de6cf7cd516021ff&lt;/code&gt; calling exploit contract &lt;code&gt;0xc8cb6a2aa825a576e1470479046056fe414ee0f8&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profit address (in-tx):&lt;/strong&gt; &lt;code&gt;0xb87cf5ab84d0ab9545358cd30229627305331373&lt;/code&gt; received &lt;strong&gt;308,959.37 USDT&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flash loan:&lt;/strong&gt; &lt;strong&gt;30,641,402.47 USDT&lt;/strong&gt; from PancakeSwap Infinity Vault &lt;code&gt;0x238a358808379702088667322f80ac48bad5e6c4&lt;/code&gt;, repaid in full in the same transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reported root cause:&lt;/strong&gt; sell-tax logic → &lt;code&gt;PoolControl.sellToken&lt;/code&gt; → &lt;code&gt;controlburn&lt;/code&gt; debiting the pair → &lt;code&gt;pair.sync()&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; the seller's remaining (~70%) transfer arrives, collapsing the recorded TENDRIL reserve to &lt;strong&gt;6,678 wei&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Chain Shows
&lt;/h2&gt;

&lt;p&gt;We pulled the receipt and decoded ERC-20 &lt;code&gt;Transfer&lt;/code&gt; and pair &lt;code&gt;Sync&lt;/code&gt; events from a public BSC node. Token0 of the pair is USDT (&lt;code&gt;0x55d39832...197955&lt;/code&gt;); token1 is TENDRIL.&lt;/p&gt;

&lt;p&gt;The flash-loan bookends are exact:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Borrow.&lt;/strong&gt; Vault &lt;code&gt;0x238a...e6c4&lt;/code&gt; sends &lt;strong&gt;30,641,402.469693 USDT&lt;/strong&gt; to the exploit contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manipulate and tax.&lt;/strong&gt; The contract buys into the thin pair, triggers TENDRIL's 30% sell tax, and routes through &lt;code&gt;PoolControl&lt;/code&gt; at &lt;code&gt;0xd00be871eb7a5cb8a69aea41e4c95affc92e54fe&lt;/code&gt;. During that path the pair's TENDRIL balance is burned (large transfer to &lt;code&gt;0xdead&lt;/code&gt;) and &lt;code&gt;sync()&lt;/code&gt; is called.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drain.&lt;/strong&gt; After the burn-sync leaves TENDRIL reserves at &lt;strong&gt;6,678 wei&lt;/strong&gt; against roughly &lt;strong&gt;4.42M USDT&lt;/strong&gt;, the seller's remaining TENDRIL arrives and a swap pulls &lt;strong&gt;4,421,023.357469 USDT&lt;/strong&gt; out of the pair to the exploit contract. The next &lt;code&gt;Sync&lt;/code&gt; shows USDT reserves at &lt;strong&gt;2 wei&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repay and keep.&lt;/strong&gt; Exactly &lt;strong&gt;30,641,402.469693 USDT&lt;/strong&gt; goes back to the vault. &lt;strong&gt;308,959.367543 USDT&lt;/strong&gt; goes to &lt;code&gt;0xb87c...1373&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That profit figure matches BlockSec Phalcon's published take of about &lt;strong&gt;308.96K USDT&lt;/strong&gt;. The &lt;strong&gt;~$550K–$556K&lt;/strong&gt; figures from CertiK, ExVul, ScaleBit and Phalcon's first alert describe the pool-side loss framing, not the single profit transfer above. Different desks also count prior setup buys differently, which is why TenArmor's early alert sat near &lt;strong&gt;$309K&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Sell Hook Broke the Pair
&lt;/h2&gt;

&lt;p&gt;Fee-on-transfer tokens on BNB Chain often take a cut on sells and push that cut through a helper. According to ExVul's public breakdown (function names as reported there; we did not verify TENDRIL source ourselves):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An ordinary sell of amount &lt;code&gt;A&lt;/code&gt; calls &lt;code&gt;PoolControl.sellToken(A)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PoolControl&lt;/code&gt; then calls sender-restricted &lt;code&gt;TENDRIL.controlburn(2A)&lt;/code&gt;, which &lt;strong&gt;debits the V2 pair itself&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PoolControl&lt;/code&gt; calls &lt;code&gt;pair.sync()&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; the seller's outer remaining transfer (described as the remaining ~70%) lands in the pair.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;msg.sender&lt;/code&gt; checks only authenticated the immediate Token and PoolControl contracts. An unprivileged seller still controlled the full &lt;code&gt;Token → PoolControl → Token → Pair&lt;/code&gt; path. Once &lt;code&gt;sync()&lt;/code&gt; had written a near-zero TENDRIL reserve, Uniswap V2-style math treated the delayed inbound TENDRIL as enough input to justify withdrawing essentially all of the USDT side.&lt;/p&gt;

&lt;p&gt;A simplified sketch of that ordering bug, written for illustration only and &lt;strong&gt;not&lt;/strong&gt; TENDRIL's actual source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration of the pattern. Not TENDRIL source.
function sellToken(uint256 taxedAmount) external onlyToken {
    // Burns from the pair balance, not from the seller's wallet.
    tendril.controlburn(taxedAmount * 2); // debits pair
    IUniswapV2Pair(pair).sync();          // locks in the burned reserve

    // DANGER: seller's remaining transfer has not arrived yet.
    // A later swapExact... call can drain USDT against the tiny reserve.
}

function _transfer(address from, address to, uint256 amount) internal {
    uint256 tax = amount * 30 / 100;
    uint256 send = amount - tax;
    // Tax path runs (and may sync) before `send` hits the pair.
    if (isSell(from, to)) {
        _poolControl.sellToken(tax);
    }
    _balances[to] += send;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safer ordering finishes every balance update that belongs to the swap &lt;strong&gt;before&lt;/strong&gt; any burn/&lt;code&gt;sync&lt;/code&gt;, or better, never lets tax logic burn the pair's own inventory at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration of a fail-closed shape.
function sellToken(uint256 taxedAmount) external onlyToken {
    // Burn from a dedicated tax treasury, never from the pair address.
    tendril.burnFrom(taxTreasury, taxedAmount);
    // If pair accounting must move, do it only after the outer
    // transfer has updated balances, and never mid-swap.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why PancakeSwap Did Not Protect LPs
&lt;/h2&gt;

&lt;p&gt;PancakeSwap V2 prices from the ratio of the two reserves it last synced. It has no opinion about whether a token burned its own side of the pool. When a custom token can change those balances and force a &lt;code&gt;sync&lt;/code&gt; in the middle of a sell, the AMM's invariant becomes a weapon against the remaining liquidity. Auditing the router does not help; the dangerous code lives in the token and its tax helper.&lt;/p&gt;

&lt;p&gt;The flash loan only rented size. With &lt;strong&gt;~30.64M USDT&lt;/strong&gt; of temporary inventory the attacker could push the thin TENDRIL pool around, trigger the taxed sell path on purpose, and still repay inside one transaction. Our &lt;a href="https://dev.to/blog/flash-loan-attack/"&gt;flash loan attack&lt;/a&gt; overview covers that funding pattern; the novel piece here is the mid-sell burn-sync, not the loan itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loss Figures and Open Questions
&lt;/h2&gt;

&lt;p&gt;Public numbers do not all measure the same thing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framing&lt;/th&gt;
&lt;th&gt;Figure&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pool / incident loss&lt;/td&gt;
&lt;td&gt;~$550K–$556K&lt;/td&gt;
&lt;td&gt;CertiK, ExVul, ScaleBit, BlockSec Phalcon first alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early monitoring estimate&lt;/td&gt;
&lt;td&gt;~$309K&lt;/td&gt;
&lt;td&gt;TenArmor Alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Profit transfer in exploit tx&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;308,959.37 USDT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;on-chain &lt;code&gt;Transfer&lt;/code&gt; we decoded; matches Phalcon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExVul campaign-profit upper bound&lt;/td&gt;
&lt;td&gt;~181,910 USDT after observed buys/gas&lt;/td&gt;
&lt;td&gt;ExVul (excludes unpriced LP basis)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Burn-sync reserve after sync&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;6,678 wei&lt;/strong&gt; TENDRIL&lt;/td&gt;
&lt;td&gt;ExVul; matches pair &lt;code&gt;Sync&lt;/code&gt; in the receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;BlockSec Phalcon later posted an update arguing that &lt;strong&gt;most overall campaign profit&lt;/strong&gt; came from roughly &lt;strong&gt;33.06B TENDRIL&lt;/strong&gt; acquired cheaply earlier through an attacker-created V3 pool, and that the burn-sync step may have added only on the order of &lt;strong&gt;$10K+&lt;/strong&gt; of incremental profit on top of that inventory. That does not erase the V2 drain mechanism above; it does mean the attacker's full P&amp;amp;L story may include setup trades outside the single exploit hash. We have not independently reconstructed that earlier V3 activity for this post.&lt;/p&gt;

&lt;p&gt;We also did not find a project post-mortem or a patched token implementation at the time of writing. Treat the function names (&lt;code&gt;sellToken&lt;/code&gt;, &lt;code&gt;controlburn&lt;/code&gt;, &lt;code&gt;_poolControl&lt;/code&gt;) as third-party labels from ExVul and BlockSec unless and until verified source is published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Builders and Auditors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never burn the pair's own balance inside a sell hook.&lt;/strong&gt; If tokenomics need a burn, burn from a treasury or from the seller's taxed amount after accounting is final. Debiting the LP pair turns every sell into a potential reserve rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not call &lt;code&gt;sync()&lt;/code&gt; until inbound transfers that belong to the swap have landed.&lt;/strong&gt; Ordering bugs are easy to miss in review because each step looks locally correct. Write a fork test that sells while a hook burns and syncs early, and assert that USDT (or the quote asset) cannot leave the pair beyond the fair amount.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authenticate the whole economic path, not just &lt;code&gt;msg.sender&lt;/code&gt;.&lt;/strong&gt; TENDRIL's checks stopped at Token and PoolControl. The unprivileged seller still drove Token → PoolControl → burn → sync. If a privileged burn exists, the only safe callers are tightly scoped and cannot be reached from arbitrary sells.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assume fee-on-transfer + flash liquidity.&lt;/strong&gt; Any tax token paired against a deep quote asset is a candidate for the same shape. Our &lt;a href="https://dev.to/blog/smart-contract-security-checklist/"&gt;smart contract security checklist&lt;/a&gt; is a practical place to park review items for custom &lt;code&gt;_transfer&lt;/code&gt; hooks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bound damage even when pricing is wrong.&lt;/strong&gt; Per-tx withdraw caps and circuit breakers on pair USDT outflows would not fix a bad burn, but they turn a full drain into a partial one and give monitors time to react.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Addresses and Figures
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chain&lt;/td&gt;
&lt;td&gt;BNB Smart Chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exploit transaction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xfdff4fba4327821c204af07944076ecaaee163a994398fdf4063d08712e40059&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block / time&lt;/td&gt;
&lt;td&gt;131,653,427 / 4 Nov 2026, 16:02:45 UTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operator (tx.from)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xb81e4aafa347b27112b7f6f5de6cf7cd516021ff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exploit contract&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xc8cb6a2aa825a576e1470479046056fe414ee0f8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Profit recipient (in-tx)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xb87cf5ab84d0ab9545358cd30229627305331373&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TENDRIL token&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xb1afdfd850da168f2cc9f742e11cdda5273973d2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TENDRIL/USDT pair&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x5de082471f2a9f01a61e467a32837dee830d99e0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PoolControl&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xd00be871eb7a5cb8a69aea41e4c95affc92e54fe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flash loan&lt;/td&gt;
&lt;td&gt;30,641,402.47 USDT (PancakeSwap Infinity Vault)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final USDT pulled from pair&lt;/td&gt;
&lt;td&gt;4,421,023.36 USDT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-tx profit transfer&lt;/td&gt;
&lt;td&gt;308,959.37 USDT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reported pool loss&lt;/td&gt;
&lt;td&gt;~$550K–$556K (multiple alert desks)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://x.com/CertiKAlert/status/2118242639212658985" rel="noopener noreferrer"&gt;CertiK Alert&lt;/a&gt;: sell hook debited pair and synced before remaining input; ~$555K net loss&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/TenArmorAlert/status/2118237717574390001" rel="noopener noreferrer"&gt;TenArmor Alert&lt;/a&gt;: same attack tx; early ~$309K estimate&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/exvulsec/status/2118221337617383455" rel="noopener noreferrer"&gt;ExVul Alert&lt;/a&gt;: &lt;code&gt;PoolControl.sellToken&lt;/code&gt; / &lt;code&gt;controlburn&lt;/code&gt; / early &lt;code&gt;sync&lt;/code&gt;; 6,678 wei reserve; addresses&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/scalebit_/status/2118235087125016845" rel="noopener noreferrer"&gt;ScaleBit Alert&lt;/a&gt;: ~550k USDT drained; pair, token, exploit, attacker addresses&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/Phalcon_xyz/status/2118307638274675165" rel="noopener noreferrer"&gt;BlockSec Phalcon&lt;/a&gt;: ~$556K losses; 30.64M flash loan; ~308.96K profit; sell-tax burn-sync root cause&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/Phalcon_xyz/status/2118335898018308197" rel="noopener noreferrer"&gt;BlockSec Phalcon update&lt;/a&gt;: later note on prior V3 accumulation vs burn-sync incremental profit&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.0xcert.com/advisory/0xCERT-2026-0313" rel="noopener noreferrer"&gt;0xCERT-2026-0313&lt;/a&gt;: advisory summarizing CertiK and TenArmor with the exploit hash&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cryptobriefing.com/tendril-exploit-555k-usdt-loss-bnb-chain/" rel="noopener noreferrer"&gt;CryptoBriefing&lt;/a&gt;: narrative summary citing ExVul / ScaleBit disclosures&lt;/li&gt;
&lt;li&gt;BNB Smart Chain receipt, &lt;code&gt;Transfer&lt;/code&gt; and &lt;code&gt;Sync&lt;/code&gt; events read directly from a public RPC node&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;TENDRIL did not lose its USDT liquidity because PancakeSwap mispriced a swap. It lost it because a sell hook was allowed to rewrite the pair's own reserves before the sell finished. Any token that taxes, burns or "recycles" against the LP pair needs the same review posture as a vault that pays from an oracle: fail closed on ordering, never let unprivileged callers drive privileged burns, and &lt;a href="https://dev.to/contact/"&gt;talk to our team&lt;/a&gt; or check our &lt;a href="https://dev.to/pricing/"&gt;audit pricing&lt;/a&gt; before that hook goes live against real liquidity.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>security</category>
      <category>blockchain</category>
      <category>solidity</category>
    </item>
    <item>
      <title>CoreVault Exploit: Spot Price Fallback Drained 259 BNB</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Wed, 04 Nov 2026 23:13:43 +0000</pubDate>
      <link>https://dev.to/pandasecurity/corevault-exploit-spot-price-fallback-drained-259-bnb-28di</link>
      <guid>https://dev.to/pandasecurity/corevault-exploit-spot-price-fallback-drained-259-bnb-28di</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/corevault-insurancepool-spot-price-oracle-exploit/" rel="noopener noreferrer"&gt;panda-security.xyz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;21:50:20 UTC on 3 November 2026&lt;/strong&gt;, a single transaction on &lt;strong&gt;BNB Smart Chain&lt;/strong&gt; drained &lt;strong&gt;259.09 BNB&lt;/strong&gt; from the reserve behind &lt;strong&gt;CoreVault&lt;/strong&gt;, a BNB vault system tied to the OSK token. The attacker kept &lt;strong&gt;160.26 BNB&lt;/strong&gt;, which CertiK valued at about &lt;strong&gt;$112,200&lt;/strong&gt;. There was no stolen key and no reentrancy. The attacker borrowed about 207,000 WBNB, dumped most of it into PancakeSwap's WBNB/USDT pool, and then asked CoreVault to redeem a position while the pool showed BNB at roughly one percent of its real price. CertiK reported that the redemption came out about &lt;strong&gt;33 times&lt;/strong&gt; larger than it should have.&lt;/p&gt;

&lt;p&gt;The interesting part is not that a spot price can be moved with a flash loan. Everyone knows that. The interesting part is that the protocol reportedly did have a multi-source oracle, and the attack went through the path the oracle used when its safer inputs were not ready yet. This post walks through what the chain shows, where the reported root cause sits, and what to test in any vault that pays out of a reserve.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; CoreVault and the InsurancePool that backs its redemptions (OSK ecosystem), BNB Smart Chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction:&lt;/strong&gt; &lt;code&gt;0x072a10a579e2a8afa26a681205a2aaaa38f74bcb9d1064b12bd9e9ebe2c598b3&lt;/code&gt;, block 131,507,810.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attacker EOA:&lt;/strong&gt; &lt;code&gt;0xd3565b191b28b49cc517189b984dacdb1ff05304&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss:&lt;/strong&gt; the reserve contract's BNB balance fell by 259.09 BNB; the attacker's balance rose by 160.26 BNB. The gap is swap fees paid to PancakeSwap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method:&lt;/strong&gt; flash loan of 207,358.41 WBNB from Lista DAO's Moolah, dump 195,000 WBNB into the PancakeSwap v2 WBNB/USDT pair, redeem at the distorted price, swap back, repay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reported root cause:&lt;/strong&gt; the redemption price came from an oracle helper that, according to one public breakdown, falls back to the PancakeSwap spot price when its TWAP and average-price windows are not fully populated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Chain Shows
&lt;/h2&gt;

&lt;p&gt;We pulled the receipt and the surrounding state from public BSC nodes. The transaction is a contract creation: the attacker deployed a helper at &lt;code&gt;0xc28cfeee...f387e2&lt;/code&gt; and ran the whole attack from its constructor, so there was no separate setup transaction to front-run or flag.&lt;/p&gt;

&lt;p&gt;The token transfers in the receipt follow a clean sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Borrow.&lt;/strong&gt; &lt;code&gt;0x8f73b65b...e5d8c&lt;/code&gt; sends &lt;strong&gt;207,358.410468 WBNB&lt;/strong&gt; to the attack contract &lt;code&gt;0x92f521a7...f542&lt;/code&gt;. Onchain Lens identified this lender as Lista DAO's Moolah, and its estimate of about $143.6M for the loan matches the amount at roughly $700 per BNB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dump.&lt;/strong&gt; The attack contract sends &lt;strong&gt;195,000 WBNB&lt;/strong&gt; into the PancakeSwap v2 pair &lt;code&gt;0x16b9a828...1eb0dae&lt;/code&gt; and receives &lt;strong&gt;13,791,474.30 USDT&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redeem.&lt;/strong&gt; A long run of OSK transfers and contract events follows between &lt;code&gt;0xfc1b8abb...159ef5&lt;/code&gt;, &lt;code&gt;0x302f5084...5d674&lt;/code&gt; and several other addresses. These are the vault's own bookkeeping as the redemption is processed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unwind.&lt;/strong&gt; The attack contract sends the same 13,791,474.30 USDT back to the pair and receives &lt;strong&gt;194,901.165115 WBNB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repay.&lt;/strong&gt; Exactly &lt;strong&gt;207,358.410468 WBNB&lt;/strong&gt; goes back to the lender. We saw no fee transfer to the lender in the receipt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The native BNB side does not show up as token transfers, so we compared balances one block before and after using an archive node:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;0xf40e0d52...53df1&lt;/code&gt; (reserve paying out)&lt;/td&gt;
&lt;td&gt;259.1618 BNB&lt;/td&gt;
&lt;td&gt;0.0691 BNB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-259.0928 BNB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;0xd3565b19...05304&lt;/code&gt; (attacker EOA)&lt;/td&gt;
&lt;td&gt;1.9114 BNB&lt;/td&gt;
&lt;td&gt;162.1692 BNB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+160.2578 BNB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The numbers reconcile. The round trip through PancakeSwap cost the attacker 195,000 minus 194,901.165, about &lt;strong&gt;98.83 WBNB&lt;/strong&gt; in fees and slippage. Take that from the 259.09 BNB that left the reserve and you get 160.26 BNB, which is what landed in the attacker's wallet. Onchain Lens reported a gross withdrawal of 260.21 BNB through CoreVault and a net depletion matching the roughly 259 BNB we see, and Telem's write-up reports the same pair of figures.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Far the Price Moved
&lt;/h2&gt;

&lt;p&gt;Right before the attack block, the PancakeSwap pair held about &lt;strong&gt;15,345,479 USDT and 21,917 WBNB&lt;/strong&gt;, a spot price of about &lt;strong&gt;$700.15 per BNB&lt;/strong&gt;. After the 195,000 WBNB dump and the USDT that came out, the reserves were about 1.55M USDT and 216,917 WBNB, which puts the spot price near &lt;strong&gt;$7.16&lt;/strong&gt;. That is roughly a 98x drop, held for the length of one transaction.&lt;/p&gt;

&lt;p&gt;CertiK reported the redemption was inflated about 33x, not 98x. We do not have the verified CoreVault source to explain the exact ratio, so treat this as our reading rather than a confirmed fact: a figure between 1x and 98x is what you would expect if the manipulated spot price was blended with other price inputs instead of used alone. Blackpaper's summary says the redemption logic averaged a same-transaction spot price into a short sampling window. Either way, a partial dilution of a 98x move still left a 33x error, which was more than enough to empty the reserve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reported Root Cause
&lt;/h2&gt;

&lt;p&gt;The most detailed public description comes from Telem, and since CoreVault has not published a post-mortem, the function names below are as reported there and not something we have verified against source code.&lt;/p&gt;

&lt;p&gt;According to that report, CoreVault's &lt;code&gt;redeemPrincipal&lt;/code&gt; pays BNB for a USD-denominated principal. To convert, it uses a value called &lt;code&gt;wbnbPerUsd&lt;/code&gt;, how many WBNB one dollar buys. That value comes from an &lt;code&gt;OracleLib&lt;/code&gt; helper that takes a median of three sources. When the TWAP or average-price windows are not fully populated, the median falls back to the PancakeSwap spot price. The payout then flows through &lt;code&gt;Treasury.payoutBnb&lt;/code&gt; into &lt;code&gt;InsurancePool.withdrawReserve&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Dumping WBNB makes WBNB cheap, so &lt;code&gt;wbnbPerUsd&lt;/code&gt; goes up, so a fixed dollar principal is converted into far more BNB. The attacker did not need to fool the oracle forever. It needed one transaction in which the only source that counted was the one it could move.&lt;/p&gt;

&lt;p&gt;A simplified sketch of that failure pattern, written for illustration only and &lt;strong&gt;not&lt;/strong&gt; CoreVault's actual code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration of the pattern. Not CoreVault's source.
function wbnbPerUsd() public view returns (uint256) {
    uint256 spot = pancakeSpotWbnbPerUsd();      // movable inside one tx
    (uint256 twap, bool twapReady) = twapWbnbPerUsd();
    (uint256 avg, bool avgReady) = avgWbnbPerUsd();

    // The dangerous branch: when safer feeds are not ready,
    // the "median" quietly becomes the spot price.
    if (!twapReady || !avgReady) {
        return spot;
    }
    return median3(spot, twap, avg);
}

function redeemPrincipal(uint256 usdPrincipal) external {
    uint256 bnbAmount = usdPrincipal * wbnbPerUsd() / 1e18;
    treasury.payoutBnb(msg.sender, bnbAmount); // pulls from the insurance reserve
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safer shape refuses to price at all when the robust inputs are missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration of a fail-closed version.
function wbnbPerUsd() public view returns (uint256 price) {
    (uint256 twap, bool twapReady) = twapWbnbPerUsd();
    require(twapReady, "oracle: twap not ready");

    uint256 spot = pancakeSpotWbnbPerUsd();
    // Spot may be used as a sanity check, never as the price.
    require(_withinBps(spot, twap, MAX_DEVIATION_BPS), "oracle: spot deviates");
    return twap;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why a Three-Source Median Did Not Help
&lt;/h2&gt;

&lt;p&gt;A median of three sources is a reasonable design. It only works if all three sources are live and independent. The reported fallback breaks both conditions at once: when the windows are empty there is effectively one source, and it is the one an attacker can move with borrowed money.&lt;/p&gt;

&lt;p&gt;Fallback paths are easy to miss in review because they look defensive. Someone wrote "if the TWAP is not ready, do not revert, use what we have" so the product would keep working right after deployment or after a quiet period. That is a liveness decision. In a function that sends value out, it turns into a security decision, and the safe default for a payout is to stop, not to guess.&lt;/p&gt;

&lt;p&gt;The second design problem is where the money came from. An insurance reserve is supposed to absorb losses. Here it was wired directly into the redemption path, so any pricing error in redemptions was paid from the backstop. That turned a pricing bug into a reserve drain, and there was no per-transaction or per-period limit in the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Builders and Auditors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Make every oracle fallback fail closed in value-moving paths.&lt;/strong&gt; If a TWAP, median or external feed is not ready, revert redemptions, withdrawals and liquidations. A paused vault is an annoyance; a vault that prices from spot is a donation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the cold start.&lt;/strong&gt; Many TWAP implementations need a warm-up period after deployment, after a pool migration, or after a long gap in observations. Write tests that call every payout function in that state and assert that they revert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use spot only as a sanity check.&lt;/strong&gt; Comparing spot to a TWAP and rejecting large deviations is useful. Letting spot set the price, even inside a median, is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bound reserve outflows.&lt;/strong&gt; Rate limits, per-transaction caps and caps relative to the reserve's size would not fix a bad price, but they turn a full drain into a partial one and buy time for monitoring to react.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model the flash loan budget.&lt;/strong&gt; Here roughly $143.6M of WBNB was borrowed against a pool holding about $30.7M of total liquidity. Your threat model should assume an attacker can borrow several times the depth of any pool your contracts read from, for free, within one transaction. Our earlier guide to &lt;a href="https://www.panda-security.xyz/blog/flash-loan-attack/" rel="noopener noreferrer"&gt;flash loan attacks&lt;/a&gt; covers the general pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat "derived from balances" and "derived from spot" as the same smell.&lt;/strong&gt; The &lt;a href="https://www.panda-security.xyz/blog/moonwell-mamo-donation-supply-cap-exploit/" rel="noopener noreferrer"&gt;Moonwell MAMO exploit&lt;/a&gt; inflated a value through direct token transfers; CoreVault inflated one through a swap. Both read a number an attacker could set. Our &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt; is a good place to start when reviewing a vault.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Still Unknown
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CoreVault has not published a post-mortem that we could find, and the contracts' source was not available to us for verification. The function-level root cause above is third-party analysis.&lt;/li&gt;
&lt;li&gt;Loss figures differ by framing. CertiK puts the attacker's take at 160.26 BNB, about $112,200. Onchain Lens and Telem put the reserve's net loss at about 259 BNB, roughly $175K to $181K depending on the BNB price used. Our balance check supports both: 259.09 BNB out of the reserve, 160.26 BNB to the attacker.&lt;/li&gt;
&lt;li&gt;We found no public report of recovered funds or of a patch to the oracle fallback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Addresses and Figures
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chain&lt;/td&gt;
&lt;td&gt;BNB Smart Chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exploit transaction&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x072a10a579e2a8afa26a681205a2aaaa38f74bcb9d1064b12bd9e9ebe2c598b3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block / time&lt;/td&gt;
&lt;td&gt;131,507,810 / 3 Nov 2026, 21:50:20 UTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attacker EOA&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xd3565b191b28b49cc517189b984dacdb1ff05304&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attack contract (created in tx)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xc28cfeee046e1421e9dbfb2cf689074bf9f387e2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flash loan&lt;/td&gt;
&lt;td&gt;207,358.410468 WBNB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WBNB dumped / recovered&lt;/td&gt;
&lt;td&gt;195,000 / 194,901.165115 WBNB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PancakeSwap spot before / during&lt;/td&gt;
&lt;td&gt;about $700.15 / about $7.16 per BNB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reserve balance change&lt;/td&gt;
&lt;td&gt;-259.0928 BNB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attacker profit&lt;/td&gt;
&lt;td&gt;+160.2578 BNB (about $112,200 per CertiK)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.techflowpost.com/en-US/newsletter/142845" rel="noopener noreferrer"&gt;CertiK alert via TechFlow&lt;/a&gt;: 33x redemption inflation, 160.26 BNB, about $112,200&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.kucoin.com/news/insight/BNB/6aeaa064bbcb52000745bcfb" rel="noopener noreferrer"&gt;Onchain Lens via KuCoin News&lt;/a&gt;: Moolah flash loan, 260.21 BNB withdrawal, attack sequence&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://telem.news/security/corevault-bnb-farming-vault-drained-181k-oracle-manipulation-attack-474e3416" rel="noopener noreferrer"&gt;Telem: CoreVault insurance pool drained&lt;/a&gt;: reported function names and oracle fallback behavior&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blackpaper.wiki/incident/BP-1387/" rel="noopener noreferrer"&gt;Blackpaper BP-1387&lt;/a&gt;: transaction hash, attacker address, spot-price averaging summary&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://thecryptoguardian.com/crypto-incidents/insurancepool-corevault-2026-11-03/" rel="noopener noreferrer"&gt;The Crypto Guardian incident record&lt;/a&gt;: SlowMist classification and loss estimate&lt;/li&gt;
&lt;li&gt;BNB Smart Chain receipt, block and archive balance data read directly from public RPC nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;CoreVault did not lose its reserve because it trusted a single DEX price on purpose. It lost it because a fallback written for convenience became the only price that mattered for one transaction. If your protocol pays out from a reserve or converts between assets with an on-chain price, &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;talk to our team&lt;/a&gt; or check our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt; to have the cold-start and fallback paths reviewed before an attacker finds them.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>defi</category>
    </item>
    <item>
      <title>Moonwell Exploit: A Donation That Skipped the MAMO Cap</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Tue, 03 Nov 2026 23:08:28 +0000</pubDate>
      <link>https://dev.to/pandasecurity/moonwell-exploit-a-donation-that-skipped-the-mamo-cap-5akm</link>
      <guid>https://dev.to/pandasecurity/moonwell-exploit-a-donation-that-skipped-the-mamo-cap-5akm</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/moonwell-mamo-donation-supply-cap-exploit/" rel="noopener noreferrer"&gt;Moonwell Exploit: A Donation That Skipped the MAMO Cap&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;27 August 2026&lt;/strong&gt;, an attacker borrowed &lt;strong&gt;$11,028,762&lt;/strong&gt; of cbBTC, WETH, USDC and wstETH from &lt;strong&gt;Moonwell&lt;/strong&gt;, a Compound v2 style lending market on &lt;strong&gt;Base&lt;/strong&gt;. The collateral behind those loans was MAMO, a thinly traded token that the attacker had formally deposited for only about &lt;strong&gt;$160K&lt;/strong&gt;. Around &lt;strong&gt;8.73M USDC&lt;/strong&gt; of the proceeds left Base through Circle's CCTP within the hour, which is where the "$8.7M exploit" headline comes from.&lt;/p&gt;

&lt;p&gt;The case is back in the news because on &lt;strong&gt;2 November 2026&lt;/strong&gt; Certora published a formal verification write-up of the bug, and it is worth studying closely. The market had a supply cap that was meant to stop exactly this kind of oversized collateral position. The cap was set correctly and it measured the right number. It simply never ran, because the attacker added most of the collateral with a plain ERC-20 &lt;code&gt;transfer&lt;/code&gt; instead of a deposit.&lt;/p&gt;

&lt;p&gt;This post explains how the attack worked, why the cap did not fire, why this is the third Compound fork hit by the same pattern in 2026, and what we check for in lending code during an audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol:&lt;/strong&gt; Moonwell, MAMO market (&lt;code&gt;mMAMO&lt;/code&gt;) on Base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date:&lt;/strong&gt; 27 August 2026, roughly 06:09 to 09:46 UTC. Post-mortem by Anthias Labs, Moonwell's risk manager, on 28 August; Certora's formal analysis on 2 November.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Borrowed:&lt;/strong&gt; $11.03M across 18 borrows. 8,729,454 USDC was burned on Base via CCTP and arrived on Ethereum, where it was swapped to DAI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Residual debt:&lt;/strong&gt; about &lt;strong&gt;$9.13M&lt;/strong&gt; of borrower obligations remained at Anthias's evidence cutoff, its best estimate of potential bad debt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; the market's cash is read from &lt;code&gt;balanceOf(address(this))&lt;/code&gt;, so tokens sent directly to the contract raise the mToken exchange rate. The 20M MAMO supply cap was only checked inside &lt;code&gt;mintAllowed&lt;/code&gt;, which a direct transfer never calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amplifier:&lt;/strong&gt; the attacker pushed the MAMO/USD feed from about &lt;strong&gt;$0.0106&lt;/strong&gt; to &lt;strong&gt;$0.4313&lt;/strong&gt; by buying on thin DEX liquidity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Moonwell's MAMO Market Looked Like
&lt;/h2&gt;

&lt;p&gt;Moonwell listed MAMO through proposal MIP-B48 on 15 October 2025 with a &lt;strong&gt;50% collateral factor&lt;/strong&gt;, a &lt;strong&gt;20,000,000 MAMO supply cap&lt;/strong&gt;, a 3,000,000 MAMO borrow cap and a 30% reserve factor. According to Certora, the listing note from Anthias said roughly $200K of MAMO could be sold instantly while keeping slippage below the 7% liquidation bonus. In other words, the cap existed to keep MAMO collateral close to what liquidators could actually sell. On the morning of the attack, 20M MAMO was worth about $212K.&lt;/p&gt;

&lt;p&gt;Like every Compound v2 fork, Moonwell gives suppliers a receipt token. One &lt;code&gt;mMAMO&lt;/code&gt; is redeemable for an amount of MAMO set by the exchange rate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moonwell's own functions update &lt;code&gt;totalBorrows&lt;/code&gt;, &lt;code&gt;totalReserves&lt;/code&gt; and &lt;code&gt;totalSupply&lt;/code&gt;. The fourth input, &lt;code&gt;totalCash&lt;/code&gt;, is not stored. It is read live from the token, as Certora quotes from &lt;code&gt;MErc20.sol&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function getCashPrior() internal view virtual override returns (uint) {
    EIP20Interface token = EIP20Interface(underlying);
    return token.balanceOf(address(this));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line is the whole vulnerability. Anything that increases the contract's MAMO balance increases what every existing &lt;code&gt;mMAMO&lt;/code&gt; is worth, whether or not it came through Moonwell.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Attack Unfolded
&lt;/h2&gt;

&lt;p&gt;Anthias Labs' post-mortem traces the funding back to &lt;strong&gt;800.098 ETH withdrawn from Tornado Cash&lt;/strong&gt; on 21 and 23 August. A linked account sold 799 ETH for &lt;strong&gt;1,947,391 USDC&lt;/strong&gt; through CoW Swap, bridged it to Base and handed MAMO, USDC and gas to the principal account (&lt;code&gt;0x719e...919D&lt;/code&gt;, shown truncated in the post-mortem). From there, all times are UTC on 27 August:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;06:09:45&lt;/td&gt;
&lt;td&gt;Linked account funds the principal account with gas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07:04:11&lt;/td&gt;
&lt;td&gt;First &lt;code&gt;mMAMO&lt;/code&gt; mint; three mints supply &lt;strong&gt;15,089,595 MAMO&lt;/strong&gt; in total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08:43:13&lt;/td&gt;
&lt;td&gt;First borrow: 0.50071233 cbBTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09:19:59&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;34,910,397 MAMO&lt;/strong&gt; sent directly to &lt;code&gt;mMAMO&lt;/code&gt;, no mint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09:21:09&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;18,482,894 MAMO&lt;/strong&gt; sent directly to &lt;code&gt;mMAMO&lt;/code&gt;, no mint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09:28:49&lt;/td&gt;
&lt;td&gt;MAMO/USD source feed peaks at &lt;strong&gt;$0.43127363&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09:29:31&lt;/td&gt;
&lt;td&gt;Moonwell accepts &lt;strong&gt;$0.40248571&lt;/strong&gt;; 368 wstETH borrowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09:30:13&lt;/td&gt;
&lt;td&gt;Final borrow: 990,000 USDC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09:30:45&lt;/td&gt;
&lt;td&gt;First liquidation, 32 seconds later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09:41:43 and 09:42:43&lt;/td&gt;
&lt;td&gt;Two CCTP burns of 4,364,726.91 USDC each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:53:43&lt;/td&gt;
&lt;td&gt;Borrow caps cut to 1 wei&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11:09:43&lt;/td&gt;
&lt;td&gt;MAMO supply cap cut to 1 wei&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 1: a legitimate looking deposit
&lt;/h3&gt;

&lt;p&gt;The formal deposit of 15.09M MAMO stayed below the 20M cap, so every &lt;code&gt;mint&lt;/code&gt; passed. It gave the attacker roughly &lt;strong&gt;735.6M mMAMO&lt;/strong&gt;, about three quarters of all &lt;code&gt;mMAMO&lt;/code&gt; in existence. That share matters: whoever owns most of the receipts captures most of any increase in the exchange rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: the donation
&lt;/h3&gt;

&lt;p&gt;The two direct transfers added &lt;strong&gt;53,393,290 MAMO&lt;/strong&gt; to the market. No &lt;code&gt;Mint&lt;/code&gt; event was emitted and &lt;code&gt;mMAMO&lt;/code&gt; total supply did not change, but cash did. Anthias measured the exchange rate rising from about &lt;strong&gt;0.020513 to 0.075460 MAMO per mMAMO&lt;/strong&gt;, a factor of &lt;strong&gt;3.68&lt;/strong&gt;. The attacker's receipts now represented about &lt;strong&gt;55.51M MAMO&lt;/strong&gt;, almost three times the cap on their own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: the price push
&lt;/h3&gt;

&lt;p&gt;In parallel, the attacker's accounts bought about &lt;strong&gt;94.31M MAMO&lt;/strong&gt; on DEXs, including Aerodrome's MAMO/USDC pool, for about $7.5M of gross volume. Much of that was recycled borrowed money. The oracle feed climbed from $0.0106 at 09:06 to $0.4313 at 09:28 and was back near $0.014 by 09:45.&lt;/p&gt;

&lt;p&gt;At Moonwell's highest accepted price, 55.51M MAMO was worth about &lt;strong&gt;$22.34M&lt;/strong&gt;, which at a 50% collateral factor allowed about $11.17M of borrowing. The attacker took $11.03M. Certora makes the point that both halves were needed: without the donation the position stays at 15M MAMO, and without the pump 55M MAMO at $0.0106 is only worth about $590K.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: borrow, bridge, walk away
&lt;/h3&gt;

&lt;p&gt;Liquidators seized essentially all of the attacker's &lt;code&gt;mMAMO&lt;/code&gt; across 595 liquidation events, but the MAMO price was already collapsing, so the seized collateral could not cover the debt. The attacker never needed to redeem the donated MAMO. Borrowing against it and leaving the debt behind was the whole plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Supply Cap Did Not Stop It
&lt;/h2&gt;

&lt;p&gt;Moonwell's cap lives in the Comptroller's &lt;code&gt;mintAllowed&lt;/code&gt; hook. Certora quotes the check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint totalSupplies = sub_(add_(totalCash, totalBorrows), totalReserves);
uint nextTotalSupplies = add_(totalSupplies, mintAmount);
require(nextTotalSupplies &amp;lt; supplyCap, "market supply cap reached");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The formula is correct: it measures everything the market owes its suppliers, including donated cash. The problem is &lt;strong&gt;when&lt;/strong&gt; it runs. It only runs on &lt;code&gt;mint&lt;/code&gt;. A MAMO &lt;code&gt;transfer&lt;/code&gt; executes inside the MAMO token contract and never calls Moonwell, so no Moonwell code can object. As Anthias put it, "the 20 million MAMO supply cap checked formal mint inputs." After the two transfers, the market would have refused any new deposit, but there was nothing left to refuse.&lt;/p&gt;

&lt;p&gt;A cap enforced at one entry point only limits that entry point. If a quantity can also change through a path you do not control, the cap is a speed bump, not a limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same Bug, Three Times in 2026
&lt;/h2&gt;

&lt;p&gt;Donation attacks on Compound forks are not new. Certora notes that Moonwell's own repository has carried a test since 2023 that reproduces the Hundred Finance exploit: it sends tokens directly to a market, and a comment says the attacker "can now borrow everything in the markets due to the massively inflated exchange rate." The tests then check that the donation cannot be pulled back out through &lt;code&gt;redeemUnderlying&lt;/code&gt;. That protects one exit, but the MAMO attacker used a different one: borrowing.&lt;/p&gt;

&lt;p&gt;In 2026 alone, Certora lists three Compound v2 forks hit by direct transfers combined with a pumped, thin collateral token: &lt;strong&gt;Venus&lt;/strong&gt; on 15 March (where, per Rekt, a position reached 367% of the cap), &lt;strong&gt;Moonwell&lt;/strong&gt; on 27 August and &lt;strong&gt;Tectonic&lt;/strong&gt; on Cronos on 30 August. We touched on Tectonic's price manipulation in our &lt;a href="https://www.panda-security.xyz/blog/0915-3/" rel="noopener noreferrer"&gt;roundup of recent blockchain hack lessons&lt;/a&gt;. The pattern is close to what happened in the &lt;a href="https://www.panda-security.xyz/blog/premia-v3-empty-option-pool-vault-exploit/" rel="noopener noreferrer"&gt;Premia v3 vault exploit&lt;/a&gt;: the code did what it was written to do, but an accounting assumption about where balances come from did not hold.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Count Only What You Received
&lt;/h2&gt;

&lt;p&gt;The robust fix is to stop deriving cash from &lt;code&gt;balanceOf&lt;/code&gt;. Certora proposes tracking cash in an internal ledger that only the market's own transfer functions move. A simplified version of their patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified for illustration, based on Certora's proposed fix.
contract MErc20DelegateInternalCash is MErc20Delegate {
    uint256 internal internalCash;

    function getCashPrior() internal view override returns (uint256) {
        return internalCash; // ignore unsolicited transfers
    }

    function doTransferIn(address from, uint256 amount)
        internal override returns (uint256)
    {
        uint256 received = super.doTransferIn(from, amount);
        internalCash += received;
        return received;
    }

    function doTransferOut(address payable to, uint256 amount)
        internal override
    {
        internalCash -= amount;
        super.doTransferOut(to, amount);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A live market would also need a one-time admin call to initialise the ledger to the current balance. Certora's AutoProver, run after the attack against commit &lt;code&gt;3b32d53&lt;/code&gt; (the last before the attack), wrote a rule stating that a donation must not change the supplier claim the cap is checked against. The Certora Prover reports that rule &lt;strong&gt;violated&lt;/strong&gt; on the deployed contract and &lt;strong&gt;verified&lt;/strong&gt; with the ledger, assuming MAMO is a standard ERC-20 with no callbacks. Moonwell, for its part, has not changed how markets count cash; it set the MAMO supply and borrow caps to 1 wei, used about $246K of reserves to repay bad debt in the USDC market (MIP-X66), and set MAMO's collateral factor to 0% from 17 October (MIP-X69).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Builders and Auditors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Never price shares from &lt;code&gt;balanceOf(address(this))&lt;/code&gt;.&lt;/strong&gt; Vault shares, lending receipts and LP tokens should all be valued from internal accounting. If you must read the balance, treat any excess as a donation that does not change share value. This belongs on every &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Enforce invariants on state, not on entry points.&lt;/strong&gt; Ask "what can change this number?" rather than "which function should check it?". If a transfer can move it, the check has to live where the number is read, or the number has to stop depending on transfers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Treat low-liquidity collateral as an oracle problem and a cap problem together.&lt;/strong&gt; A cap sized to liquidation depth is only meaningful if nothing can grow positions past it. Pair caps with price feeds that resist minute-scale manipulation, such as TWAPs or circuit breakers on sudden moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Test the exits you did not think of.&lt;/strong&gt; Moonwell tested that a donation could not be redeemed. It did not test that a donation could not be borrowed against. For each attack class, enumerate every way value can leave: redeem, borrow, liquidate, transfer receipts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Monitor for unsolicited transfers.&lt;/strong&gt; A 35M token transfer into a market contract with no &lt;code&gt;Mint&lt;/code&gt; event is a loud, cheap signal. An alert on that would have given Moonwell about ten minutes before the final borrows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Addresses and Figures
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chain&lt;/td&gt;
&lt;td&gt;Base&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Principal account (truncated in post-mortem)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x719e...919D&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linked operational account (truncated)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xD71d...C384&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formal MAMO supplied&lt;/td&gt;
&lt;td&gt;15,089,595 MAMO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct MAMO transfers&lt;/td&gt;
&lt;td&gt;53,393,290 MAMO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exchange rate change&lt;/td&gt;
&lt;td&gt;0.020513 to 0.075460 MAMO per mMAMO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total borrowed&lt;/td&gt;
&lt;td&gt;$11,028,762 (18 borrows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USDC burned via CCTP&lt;/td&gt;
&lt;td&gt;8,729,453.83 USDC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Residual debt at evidence cutoff&lt;/td&gt;
&lt;td&gt;about $9,131,342&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://forum.moonwell.fi/t/post-mortem-mamo-market-incident-on-base/2208" rel="noopener noreferrer"&gt;Anthias Labs: Post-Mortem, MAMO Market Incident on Base (Moonwell forum)&lt;/a&gt;: timeline, amounts, exchange rate, borrows, liquidations and fund flows&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.certora.com/blog/moonwell-exploit-supply-cap-bypass-attack" rel="noopener noreferrer"&gt;Certora: The Moonwell Exploit, an $8.7M Supply Cap Bypass Attack&lt;/a&gt;: code paths, cap logic, prior donation attacks, AutoProver results and status as of 28 October&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://olympix.security/exploits/moonwells-8-7m-loss-and-how-olympix-would-have-approached-it" rel="noopener noreferrer"&gt;Olympix: Moonwell $8.7M exploit analysis&lt;/a&gt;: independent summary of the borrowed total and timing&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.quillaudits.com/blog/hack-analysis/moonwell-donation-exploit" rel="noopener noreferrer"&gt;QuillAudits: Moonwell mMAMO exchange rate inflation exploit&lt;/a&gt;: independent description of the direct-transfer path&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The Moonwell exploit is a reminder that a correct check in the wrong place is not a control. The supply cap measured the right number, and the attack class had even been tested for years earlier, yet $11M walked out through a path nobody had pinned down. If your protocol values shares, receipts or collateral from token balances, &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;talk to our team&lt;/a&gt; or see our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt; to have those accounting assumptions reviewed before someone tests them on mainnet.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>defi</category>
      <category>solidity</category>
    </item>
    <item>
      <title>b14g Exploit: Withdrawing Zero CORE Drained 97M CORE</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Mon, 02 Nov 2026 23:07:16 +0000</pubDate>
      <link>https://dev.to/pandasecurity/b14g-exploit-withdrawing-zero-core-drained-97m-core-4a9b</link>
      <guid>https://dev.to/pandasecurity/b14g-exploit-withdrawing-zero-core-drained-97m-core-4a9b</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/b14g-core-zero-amount-withdrawal-exploit/" rel="noopener noreferrer"&gt;panda-security.xyz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;09:29:51 UTC on 31 October 2026&lt;/strong&gt;, a single transaction on the &lt;strong&gt;Core&lt;/strong&gt; blockchain emptied &lt;strong&gt;91 staking orders&lt;/strong&gt; held by &lt;strong&gt;b14g&lt;/strong&gt;, a Bitcoin dual-staking protocol. The attacker did not steal a key, deploy a new contract or bend a price oracle. They sent b14g a list of &lt;strong&gt;99 withdrawal requests, each for 0 CORE&lt;/strong&gt;. Core's staking layer reads a zero amount as "withdraw everything", so every order paid out in full: &lt;strong&gt;97,045,563 CORE&lt;/strong&gt;, worth about &lt;strong&gt;$1.47M&lt;/strong&gt; at the pre-attack price according to Bitquery.&lt;/p&gt;

&lt;p&gt;Two days later the story took an unusual turn. On &lt;strong&gt;2 November&lt;/strong&gt;, a Core network hard fork moved &lt;strong&gt;68,146,177.22 CORE&lt;/strong&gt; out of three attacker-controlled addresses into a recovery address. That is roughly 70% of the loss, clawed back by changing the chain's rules rather than by any transaction the attacker signed.&lt;/p&gt;

&lt;p&gt;This post walks through the bug, the on-chain evidence, and the lessons for anyone building on top of somebody else's staking or vault interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  What b14g Does
&lt;/h2&gt;

&lt;p&gt;b14g matches two kinds of users on Core. Bitcoin holders lock BTC in a time lock on the Bitcoin chain, and CORE holders stake CORE alongside them so both sides earn Core's higher "dual staking" yield. On the Core side, b14g represents each arrangement as an &lt;strong&gt;order&lt;/strong&gt;: a small contract that holds CORE and keeps it delegated to a Core validator.&lt;/p&gt;

&lt;p&gt;To withdraw, a user calls b14g's main contract with three pieces of data per entry: the order, the validator it delegates to, and how much CORE to take out. b14g forwards the request to Core's system staking contract, which releases the coins. According to Bitquery, six wallets made eight ordinary withdrawals through this path between 28 October and the attack, each asking for a specific amount and receiving exactly that.&lt;/p&gt;

&lt;p&gt;The BTC side was not touched. b14g says Bitcoin staked through it stays time-locked on Bitcoin and was not affected; only CORE was drained.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack Transaction
&lt;/h2&gt;

&lt;p&gt;We read the attack transaction directly from Core's public RPC to check the reported figures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transaction:&lt;/strong&gt; &lt;code&gt;0xe1947f325e1f0bc22290ea97dc5b2ce44827fbb56e1cc40abaeda1d2ed04f62c&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block:&lt;/strong&gt; 40,042,125, timestamp 2026-10-31 09:29:51 UTC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sender:&lt;/strong&gt; &lt;code&gt;0xc556ed208285406d71705927f92d4f4eaea579f1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Called contract:&lt;/strong&gt; &lt;code&gt;0x04ea61c431f7934d51fed2acb2c5f942213f8967&lt;/code&gt; (b14g's proxy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; success, 11,464,357 gas, 327 event logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The calldata is a single dynamic array with &lt;strong&gt;99 entries&lt;/strong&gt;. Each entry is a tuple of (order address, validator address, amount). Decoding it, we found that &lt;strong&gt;every amount is zero&lt;/strong&gt;, the entries point at &lt;strong&gt;91 distinct orders&lt;/strong&gt;, and those orders are spread across &lt;strong&gt;23 validators&lt;/strong&gt;. The receipt also contains exactly 99 events from b14g's contract and 99 matching events from Core's system contract at &lt;code&gt;0x...1011&lt;/code&gt;, which lines up with one forwarded request per entry.&lt;/p&gt;

&lt;p&gt;There are no ERC-20 Transfer events in the receipt. CORE is the chain's native asset, so the 97 million moved as native value through internal calls. Monitoring that only watches token Transfer logs would have seen nothing.&lt;/p&gt;

&lt;p&gt;The sender wallet was fresh. Bitquery reports that its only funding was 615.62 CORE for gas, sent 29 minutes before the drain, and that the attack call was its first interaction with b14g.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause: A Sentinel Value Meets a Missing Ownership Check
&lt;/h2&gt;

&lt;p&gt;Two separate design decisions combined into one exploit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero means "all" in Core's staking layer
&lt;/h3&gt;

&lt;p&gt;Core's staking code treats a withdrawal amount of 0 as a request to withdraw the delegator's entire stake with that validator. Each b14g order is the delegator from Core's point of view. So when b14g forwarded &lt;code&gt;amount = 0&lt;/code&gt; on behalf of an order, Core released that order's full balance. Core DAO said its consensus, validators and staking contracts kept working normally, and Bitquery's reading of the chain agrees: the system contract paid out exactly what it was asked to pay out.&lt;/p&gt;

&lt;p&gt;Using 0 (or &lt;code&gt;type(uint256).max&lt;/code&gt;) as "everything" is a common convenience in DeFi interfaces. On its own it is harmless. It becomes dangerous the moment another contract passes user-controlled amounts straight into it.&lt;/p&gt;

&lt;h3&gt;
  
  
  b14g did not check who was asking
&lt;/h3&gt;

&lt;p&gt;The second half is on b14g's side. The attacker's wallet had funded none of the 91 orders, yet b14g accepted withdrawal requests against all of them and sent the proceeds to the caller. Bitquery notes it has not read b14g's source and cannot point to the exact line; b14g has promised a full post-mortem. What the chain does show is that a wallet with no stake asked for nothing, against orders it did not own, and was paid everything.&lt;/p&gt;

&lt;p&gt;Bitquery also found that the logic running at the time had been b14g's standing implementation for about &lt;strong&gt;17 months&lt;/strong&gt;, so this was not a fresh, rushed upgrade.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the pattern looks like (simplified)
&lt;/h3&gt;

&lt;p&gt;The Solidity below is &lt;strong&gt;simplified and illustrative&lt;/strong&gt;. It is not b14g's or Core's code. It shows how the two halves interact.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration only - not the real b14g or Core contracts.
interface IStakeHub {
    // amount == 0 means "withdraw the full stake"
    function undelegate(address validator, uint256 amount) external;
}

contract VulnerableRouter {
    struct Req { address order; address validator; uint256 amount; }

    function batchWithdraw(Req[] calldata reqs) external {
        for (uint256 i; i &amp;lt; reqs.length; ++i) {
            // BUG 1: no check that msg.sender owns reqs[i].order
            // BUG 2: amount 0 is forwarded unchanged and becomes "all"
            uint256 got = IOrder(reqs[i].order).withdraw(
                reqs[i].validator, reqs[i].amount
            );
            payable(msg.sender).transfer(got);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fixed version closes both doors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration only.
function batchWithdraw(Req[] calldata reqs) external {
    for (uint256 i; i &amp;lt; reqs.length; ++i) {
        Req calldata r = reqs[i];
        require(orderOwner[r.order] == msg.sender, "not owner");
        require(r.amount &amp;gt; 0, "zero amount");
        require(r.amount &amp;lt;= stakeOf[r.order], "exceeds stake");

        uint256 before = address(this).balance;
        IOrder(r.order).withdraw(r.validator, r.amount);
        uint256 got = address(this).balance - before;
        require(got == r.amount, "unexpected payout");

        stakeOf[r.order] -= got;
        (bool ok, ) = msg.sender.call{value: got}("");
        require(ok, "send failed");
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ownership check is the real fix. Rejecting zero and asserting the received amount are defence in depth, so that a misunderstood downstream convention cannot silently turn into "send everything".&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeline
&lt;/h2&gt;

&lt;p&gt;All times UTC, from Bitquery's reconstruction, ChainClarity's trace and b14g's posts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;31 Oct 09:00:42&lt;/td&gt;
&lt;td&gt;Attacker wallet receives 615.62 CORE for gas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31 Oct 09:29:51&lt;/td&gt;
&lt;td&gt;Drain transaction: 99 zero-amount requests, 91 orders, 97,045,563 CORE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31 Oct 09:37 to 10:20&lt;/td&gt;
&lt;td&gt;About 23.4M CORE moved out through single-use relay addresses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31 Oct 10:35:51&lt;/td&gt;
&lt;td&gt;b14g multisig pauses the contract (about 66 minutes after the drain)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31 Oct 10:36:33&lt;/td&gt;
&lt;td&gt;Attacker moves 73,646,178 CORE to a second wallet, &lt;code&gt;0x27758c30f8e424fbd8ca605bf31e9496c26ce92c&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;31 Oct 16:38&lt;/td&gt;
&lt;td&gt;b14g posts its security update on X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 Nov 04:00 (scheduled)&lt;/td&gt;
&lt;td&gt;Core hard fork activates and moves 68,146,177.22 CORE to a recovery address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 Nov 07:23&lt;/td&gt;
&lt;td&gt;b14g publishes "Recovery Update #1"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;b14g says it paused the contracts as soon as it discovered the problem, and the chain cannot show when that was. What the chain does show is that the pause landed 66 minutes after the drain. By then about 23M CORE had already left the first wallet. Some of it reached exchange deposit addresses, and b14g says exchange partners froze about 9M CORE.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard-Fork Recovery
&lt;/h2&gt;

&lt;p&gt;According to b14g's recovery update and reporting by The Defiant, Core's &lt;strong&gt;Fermi&lt;/strong&gt; upgrade included a one-time state change: it set the balances of three attacker addresses to zero and credited the combined amount, &lt;strong&gt;68,146,177.22 CORE&lt;/strong&gt;, to a designated recovery address. b14g describes this as made "without reversing transactions, minting new tokens, or changing the total CORE supply". When we queried Core's RPC late on 2 November (UTC), the second attacker wallet held 0 CORE, which matches that account.&lt;/p&gt;

&lt;p&gt;As of b14g's update, the recovered CORE has not been distributed, no official claims process exists, and the exchange-frozen funds have not been returned. The Defiant estimates that around 19.9M CORE sits outside both the fork recovery and the reported freezes, while noting the figure is provisional.&lt;/p&gt;

&lt;p&gt;For users this is good news. For protocol designers it is a reminder of something else: the recovery depended on a chain-level intervention that most applications cannot count on. On Ethereum or a major L2, there would be no fork. Treat a hard fork as a rare outcome, not as a plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Builders and Auditors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Map every sentinel value in the interfaces you call.&lt;/strong&gt; Zero-means-all, max-uint-means-all, &lt;code&gt;address(0)&lt;/code&gt;-means-default: list them for each external dependency and decide, explicitly, whether your contract rejects them or authorises them. Our &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt; has a section on external call assumptions that fits here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Ownership checks belong on every path that moves user funds.&lt;/strong&gt; The missing control was not input sanitisation; it was "whose position is this?". The same family of failure showed up when a &lt;a href="https://www.panda-security.xyz/blog/troves-starknet-legacy-rebalancer-exploit/" rel="noopener noreferrer"&gt;retired rebalancer kept vault permissions at Troves&lt;/a&gt;, and in the &lt;a href="https://www.panda-security.xyz/blog/wax-bridge-negative-fee-exploit/" rel="noopener noreferrer"&gt;WAX bridge&lt;/a&gt;, where one unchecked number drained the contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Assert what you received, not what you asked for.&lt;/strong&gt; Measuring the balance delta and comparing it with the requested amount turns a silent overpayment into a revert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Batch functions multiply blast radius.&lt;/strong&gt; A batch withdraw is convenient for users and for attackers. Here one call emptied 91 positions. Per-call or per-block outflow limits would have capped the damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Watch native-asset flows, and rehearse the pause.&lt;/strong&gt; Native transfers emit no ERC-20 events, so monitoring has to track balances and internal calls. And 66 minutes from drain to pause is a measurable number; incident playbooks should aim to make it minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Time in production is not proof of safety.&lt;/strong&gt; The vulnerable logic ran for roughly 17 months. That measures how long nobody looked, not whether it was correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Addresses and Transactions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Drain transaction (block 40,042,125)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xe1947f325e1f0bc22290ea97dc5b2ce44827fbb56e1cc40abaeda1d2ed04f62c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attacker wallet A (sender)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xc556ed208285406d71705927f92d4f4eaea579f1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attacker wallet B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x27758c30f8e424fbd8ca605bf31e9496c26ce92c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b14g contract (proxy)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x04ea61c431f7934d51fed2acb2c5f942213f8967&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;b14g multisig&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xff2a4d8f696991d5350b28fd6fbcf3c7b7f94233&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://x.com/b14g_network/status/2116570456245596580" rel="noopener noreferrer"&gt;b14g Security Update on X&lt;/a&gt;: ~97M CORE withdrawn at 09:29 UTC, contracts paused&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/b14g_network/status/2117155664980251068" rel="noopener noreferrer"&gt;b14g Recovery Update #1 on X&lt;/a&gt;: 68,146,177.22 CORE recovered via hard fork, 97,045,563 CORE affected&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bitquery.io/investigations/b14g-hack" rel="noopener noreferrer"&gt;Bitquery: b14g Hack, 97M CORE Taken by Asking to Withdraw Zero&lt;/a&gt;: mechanism, timeline, 66-minute pause, fund flows and addresses&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chainclarity.cc/scam-database/a-withdrawal-request-for-zero-core-meant-withdraw-everything-and-emptied-91-b14g-staking-positions-in-one-transaction/" rel="noopener noreferrer"&gt;ChainClarity: zero-CORE withdrawal emptied 91 b14g positions&lt;/a&gt;: transaction receipt details and relay trace&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cryptonews.net/news/security/33688972/" rel="noopener noreferrer"&gt;The Defiant via Cryptonews.net: Core fork recovers 68.15M CORE&lt;/a&gt;: Fermi upgrade details and remaining shortfall&lt;/li&gt;
&lt;li&gt;Core public RPC (&lt;code&gt;rpc.coredao.org&lt;/code&gt;): transaction, receipt, calldata decoding, block timestamp and current balances, read by PandaSecurity&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The b14g exploit is a clean example of a composability bug. Core's staking contract kept its documented promise, and b14g's withdrawal path had worked for months. The loss came from the seam between them, where a harmless convenience in one contract became a wildcard in another because nobody asked who owned the position. If your protocol wraps staking, vaults or any external system with "magic" inputs, &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;talk to our team&lt;/a&gt; or check our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt; to get those integration boundaries reviewed before someone else reviews them for you.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>solidity</category>
    </item>
    <item>
      <title>Premia Exploit: Empty Option Pools Drained Six Vaults</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Sun, 01 Nov 2026 23:07:43 +0000</pubDate>
      <link>https://dev.to/pandasecurity/premia-exploit-empty-option-pools-drained-six-vaults-32ai</link>
      <guid>https://dev.to/pandasecurity/premia-exploit-empty-option-pools-drained-six-vaults-32ai</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/premia-v3-empty-option-pool-vault-exploit/" rel="noopener noreferrer"&gt;Premia Exploit: Empty Option Pools Drained Six Vaults&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;1 November 2026&lt;/strong&gt;, an attacker emptied six &lt;strong&gt;Premia v3 underwriter vaults on Arbitrum&lt;/strong&gt; in two transactions sent one second apart. The vaults lost &lt;strong&gt;3.644 WBTC, 43.47 WETH, 48,479 USDC.e and 46,215 ARB&lt;/strong&gt;, which The Defiant valued at about &lt;strong&gt;$437,000&lt;/strong&gt;. Blockaid flagged the attack while it was still running and described the method in one sentence: the attacker created empty option pools, flash-loaned capital, bought options so that vault collateral was locked into those pools, and then withdrew that collateral as liquidity.&lt;/p&gt;

&lt;p&gt;No key was stolen and no oracle was bent. The vaults did what their code allowed: they underwrote options in markets that the attacker had created a minute earlier. This post walks through what Premia's vaults do, what the chain shows, what is still unknown, and what builders and auditors of options and structured-product vaults should take from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol:&lt;/strong&gt; Premia v3, an options protocol. Affected contracts: six "Short Volatility" underwriter vaults (WBTC, WETH and ARB calls and puts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain:&lt;/strong&gt; Arbitrum One.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss:&lt;/strong&gt; 3.64410382 WBTC, about 43.47 WETH, 48,479.36 USDC.e and 46,215.43 ARB in net outflows, about $437K (The Defiant). Blockaid's mid-attack estimate was about $400.5K; PeckShield reported about $464.6K.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method (as reported by Blockaid):&lt;/strong&gt; permissionlessly create empty option pools, borrow working capital from Balancer, buy options from the vaults so their collateral moves into those pools, then take the collateral out as liquidity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; not yet confirmed by Premia. The evidence points at the trust boundary between the vaults and pools that anyone can deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Funds:&lt;/strong&gt; PeckShield reported the proceeds were bridged to Ethereum and swapped to BTC through Chainflip and NEAR Intents.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Premia v3 Vaults Do
&lt;/h2&gt;

&lt;p&gt;Premia v3 splits options trading into two layers. &lt;strong&gt;Pools&lt;/strong&gt; are per-series markets: one pool per base/quote pair, strike, maturity and call/put flag, deployed through a factory. Anyone can create a new series. &lt;strong&gt;Underwriter vaults&lt;/strong&gt; sit above the pools. Depositors put a single asset into a vault, and the vault uses it as collateral to sell (write) options to buyers, collecting premiums in return. A call vault holds the base asset (WBTC, WETH or ARB); a put vault holds USDC.e.&lt;/p&gt;

&lt;p&gt;When a buyer trades with a vault, the vault writes options into the matching pool. Writing an option means locking collateral in that pool so the buyer can be paid if the option ends in the money. That collateral leaves the vault's balance sheet and sits in the pool until the series settles.&lt;/p&gt;

&lt;p&gt;This design is common. It is also exactly where an attacker would look first: the vault holds real deposits, and it will move them into a pool contract on request, provided the trade passes the vault's checks. If those checks treat every factory-deployed pool as a legitimate market, the security of depositors' money depends on how a brand-new, empty pool behaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened On-Chain
&lt;/h2&gt;

&lt;p&gt;We read the blocks and receipts directly from a public Arbitrum node. The sequence is short.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Setup, 03:14:37 UTC, block 520,166,285.&lt;/strong&gt; The attacker address &lt;code&gt;0xbeBa8CEfD0D75d747C5AD966Dc17C0Bb60D84dBd&lt;/code&gt; called its contract &lt;code&gt;0x577168C8c6fA937495C3D6961f1d7D734b2d785f&lt;/code&gt; (transaction &lt;code&gt;0xe3c263fc...31f5&lt;/code&gt;). Reporting relayed by KuCoin News says this transaction created the six option-pool contracts used later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First drain, 03:15:39 UTC, block 520,166,479&lt;/strong&gt; (&lt;code&gt;0x64602cbb...41ac&lt;/code&gt;). The attack contract borrowed tokens from the Balancer Vault, including 0.9 WBTC, 11 WETH and 6,000 USDC.e. Option tokens with names such as &lt;code&gt;WBTC-USDC-06NOV2026-78000-C&lt;/code&gt; and &lt;code&gt;WETH-USDC-06NOV2026-2200-P&lt;/code&gt; were minted and burned, collateral moved from the vaults into the new pools and then on to the attack contract, and Balancer was repaid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Second drain, 03:15:40 UTC, block 520,166,483&lt;/strong&gt; (&lt;code&gt;0xd7e8086d...44ff&lt;/code&gt;). Same sender, same contract, same selector. This one borrowed 12,000 ARB and 9,500 USDC.e and repeated the pattern against the ARB vaults.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both drain transactions succeeded, and each produced close to 400 log entries. Summing only the ERC-20 transfers into and out of the six vault addresses across both transactions gives these net outflows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vault&lt;/th&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;Net outflow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WBTC call&lt;/td&gt;
&lt;td&gt;WBTC&lt;/td&gt;
&lt;td&gt;3.644104&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WETH call&lt;/td&gt;
&lt;td&gt;WETH&lt;/td&gt;
&lt;td&gt;43.472546&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARB call&lt;/td&gt;
&lt;td&gt;ARB&lt;/td&gt;
&lt;td&gt;46,215.428616&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WBTC put&lt;/td&gt;
&lt;td&gt;USDC.e&lt;/td&gt;
&lt;td&gt;20,215.067131&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WETH put&lt;/td&gt;
&lt;td&gt;USDC.e&lt;/td&gt;
&lt;td&gt;21,333.613826&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARB put&lt;/td&gt;
&lt;td&gt;USDC.e&lt;/td&gt;
&lt;td&gt;6,930.677768&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three put vaults together lost 48,479.36 USDC.e, which matches The Defiant's figure exactly. After the attack, the WBTC call vault held about 0.00087 WBTC and the put vaults between about 45 and 126 USDC.e. By TVL, that is most of what Premia v3 had on Arbitrum: DefiLlama showed roughly $533K just before the attack.&lt;/p&gt;

&lt;p&gt;The flash loans were only working capital. They were repaid inside the same transactions. The profit came entirely from depositor collateral.&lt;/p&gt;

&lt;p&gt;Coindaro also found ten smaller outflows from the same vaults over the next two hours, worth about $16K in total, from other addresses. Who sent them is not known; they may be copycats, the same actor or a rescue effort. They are not included in the figures above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause: Vaults That Trust Any Pool
&lt;/h2&gt;

&lt;p&gt;Premia has not published a post-mortem, so the precise vulnerable function is not confirmed. What is public is enough to describe the shape of the bug.&lt;/p&gt;

&lt;p&gt;The key ingredients are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permissionless markets.&lt;/strong&gt; The attacker deployed the six fresh pools, all with a 6 November 2026 expiry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A vault willing to underwrite into them.&lt;/strong&gt; The vaults accepted trades against those new series and locked their own collateral there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A pool that let that collateral leave.&lt;/strong&gt; Once inside a pool that only the attacker had touched, the collateral came back out to the attacker's contract as if it were the attacker's own liquidity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the vault's assumption was "a pool deployed by our factory is a safe place for our collateral". That only holds if every state a pool can be in, including empty, first-trade and single-participant states, keeps the vault's collateral attributed to the vault. An empty pool is the most extreme edge case there is: the attacker is the only other counterparty, sets the opening conditions, and can sequence every step inside one transaction.&lt;/p&gt;

&lt;p&gt;The snippet below is a &lt;strong&gt;simplified, hypothetical&lt;/strong&gt; illustration of that trust pattern. It is not Premia's code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SIMPLIFIED ILLUSTRATION - not Premia's actual implementation
function trade(PoolKey calldata key, uint256 size, uint256 maxPremium) external {
    // Any pool the factory can derive is treated as a valid market
    address pool = factory.getPoolAddress(key);
    require(pool != address(0), "no pool");
    require(_isValidListing(key), "bad strike or maturity");

    uint256 premium = _quote(key, size);
    require(premium &amp;lt;= maxPremium, "slippage");
    asset.safeTransferFrom(msg.sender, address(this), premium);

    // Vault collateral leaves the vault and is locked in the pool
    uint256 collateral = _collateralFor(key, size);
    asset.approve(pool, collateral);
    IPool(pool).writeFrom(address(this), msg.sender, size);

    // From here on, the vault relies entirely on the pool's accounting
    // to keep that collateral attributed to the vault.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in this sketch is obviously wrong in isolation. Strike and maturity are checked, the premium has a slippage bound, and the pool is a genuine factory deployment. The problem is the last comment. If pool-side accounting can be steered by whoever created the pool, especially when the pool has no other liquidity, a vault-level check that only asks "is this a real pool?" offers no protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Brand-New Markets Are Dangerous
&lt;/h2&gt;

&lt;p&gt;The Premia case joins a long list of exploits where the first moments of a contract's life were the weak point. Empty ERC-4626 vaults are vulnerable to share inflation. Fresh lending markets have been drained through rounding on the first deposit. And a flash loan let an attacker dictate a thin pool's spot price for one transaction in the &lt;a href="https://www.panda-security.xyz/blog/kolan-hook-exploit-analysis/" rel="noopener noreferrer"&gt;KolanHook exploit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Empty markets are dangerous for three reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The attacker is the whole market.&lt;/strong&gt; No other liquidity providers or traders constrain prices, balances or ordering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge-case arithmetic is live.&lt;/strong&gt; Zero totals, first-position branches and division by small numbers are paths that normal tests rarely exercise with hostile inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomicity removes risk.&lt;/strong&gt; With a flash loan, the attacker can create conditions, extract value and repay inside one transaction, so failure costs only gas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also an operational angle. Premia's team now builds Kyan, its newer platform, but the v3 contracts were still live and still held deposits. As we saw last week with the &lt;a href="https://www.panda-security.xyz/blog/troves-starknet-legacy-rebalancer-exploit/" rel="noopener noreferrer"&gt;Troves legacy rebalancer&lt;/a&gt;, code a team has stopped watching keeps running for as long as it holds funds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Builders and Auditors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Allowlist the markets a vault may underwrite.&lt;/strong&gt; Composability is great for traders and dangerous for pooled collateral. A vault that holds depositors' assets should only write into pools that governance or a curator has approved, or at minimum into pools that meet liquidity, age and parameter requirements. "Deployed by our factory" is not a risk check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Treat pool creation as an attacker capability.&lt;/strong&gt; In threat models for permissionless factories, assume the attacker deploys the pool, chooses its parameters and is the only counterparty. Every function a vault calls on a pool should be reviewed under that assumption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Verify collateral attribution end to end.&lt;/strong&gt; After a vault writes into a pool, write invariant tests that check the vault's claim on that collateral cannot fall unless an option is exercised against it. Fuzz the sequences create pool, trade, add or remove liquidity, settle, all from one actor and inside one transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Cap per-market and per-block exposure.&lt;/strong&gt; Six vaults lost almost everything within one second. Limits on how much collateral can enter a single new series, or how much a vault can underwrite per block, would have bounded the damage even with the underlying bug present.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Sunset old deployments deliberately.&lt;/strong&gt; When a protocol moves to a new version, either wind down the old vaults, pause new underwriting, or keep monitoring and bug-bounty coverage in place. Our &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt; covers the monitoring side of this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Monitor for the pattern, not just the loss.&lt;/strong&gt; A single address creating several pools and then trading against vaults in the next minute is an unusual sequence. Alerts on new-series creation followed by large vault trades can buy time for a pause.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Still Unknown
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The exact vulnerable function.&lt;/strong&gt; Blockaid's description fits either a vault-side or a pool-side flaw; no one has publicly pinned it to a specific line of code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The final loss figure.&lt;/strong&gt; $400.5K (Blockaid, during the attack), about $437K (The Defiant, net collateral at prices around 04:16 UTC) and $464.6K (PeckShield) measure different things and should not be added together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Premia's response.&lt;/strong&gt; At the time of writing we found no official statement, post-mortem, pause notice or bounty offer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The later outflows.&lt;/strong&gt; The ten smaller withdrawals reported by Coindaro are not attributed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Addresses and Transactions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attacker address (tx sender)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xbeBa8CEfD0D75d747C5AD966Dc17C0Bb60D84dBd&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attack contract&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x577168C8c6fA937495C3D6961f1d7D734b2d785f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup tx (block 520,166,285)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xe3c263fcae544954367998e48e13112875cfd38a3b9a240e7818bb96313831f5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drain tx 1 (block 520,166,479)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x64602cbbeba5d3c7a5e2cff4f98cc118886fc6051c56fa50458abec2c90441ac&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drain tx 2 (block 520,166,483)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xd7e8086df0570c5389ca27eb9bcdff290e810c094155b410a42e5bad2b3244ff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WBTC call vault&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x45dF5EA836A15B561937Ae8373ab9eE984aea531&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WETH call vault&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xbae43c546DFADC69576aE73C68e0694A54F08e1B&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARB call vault&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xDC631d88dbB5eb39f5c4Bf8B4e5298d098912fFf&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WBTC put vault&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x6A1FC0E1c60BA6564CDe4910A425F1F1a1d18C1F&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WETH put vault&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xD46993F25D298ebbCD31E941156C66f7e628A52a&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARB put vault&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xBe3E229319f86f5ee96ee1dc0b6d55e8b68a439e&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://x.com/blockaid_/status/2116745378674463229" rel="noopener noreferrer"&gt;Blockaid on X: live exploit alert&lt;/a&gt;: method description and the ~$400.5K mid-attack estimate&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arbiscan.io/tx/0x64602cbbeba5d3c7a5e2cff4f98cc118886fc6051c56fa50458abec2c90441ac" rel="noopener noreferrer"&gt;Arbiscan: first drain transaction&lt;/a&gt; and &lt;a href="https://arbiscan.io/tx/0xd7e8086df0570c5389ca27eb9bcdff290e810c094155b410a42e5bad2b3244ff" rel="noopener noreferrer"&gt;second drain transaction&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cryptonews.net/news/security/33683798/" rel="noopener noreferrer"&gt;The Defiant via Cryptonews.net: attacker drains about $437,000 from Premia vaults&lt;/a&gt;: per-asset net outflows and post-attack balances&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.coindaro.com/premia-exploit-arbitrum-option-vaults-flash-loan/" rel="noopener noreferrer"&gt;Coindaro: six Arbitrum vaults lose about $437,000&lt;/a&gt;: block numbers, vault addresses, flash-loan amounts and later outflows&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.0xcert.com/advisory/0xCERT-2026-0297" rel="noopener noreferrer"&gt;0xCERT-2026-0297 advisory&lt;/a&gt;: PeckShield's $464.6K figure and the Chainflip / NEAR Intents exit route&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tsnmedia.org/premia-exploit-arbitrum-vaults/" rel="noopener noreferrer"&gt;TSN: Premia exploit drains six Arbitrum options vaults&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Arbitrum blocks and transaction receipts read from the public Arbitrum One RPC: timestamps, senders, status and ERC-20 transfer totals&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The Premia exploit did not need a price oracle, a stolen key or a novel cryptographic trick. It needed a vault that would put real collateral into a market nobody else had ever used. If your protocol lets anyone create markets and lets pooled capital flow into them, the empty-market case is your threat model, not an edge case. If you are building vaults, options or any permissionless market factory, &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;talk to our team&lt;/a&gt; or see our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt; to have those trust boundaries reviewed before an attacker reviews them for you.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>defi</category>
      <category>solidity</category>
    </item>
    <item>
      <title>Troves Exploit: A Retired Rebalancer Drained 8.57 WBTC</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Sat, 31 Oct 2026 23:10:37 +0000</pubDate>
      <link>https://dev.to/pandasecurity/troves-exploit-a-retired-rebalancer-drained-857-wbtc-2009</link>
      <guid>https://dev.to/pandasecurity/troves-exploit-a-retired-rebalancer-drained-857-wbtc-2009</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/troves-starknet-legacy-rebalancer-exploit/" rel="noopener noreferrer"&gt;Troves Exploit: A Retired Rebalancer Drained 8.57 WBTC&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;31 October 2026&lt;/strong&gt;, an attacker removed about &lt;strong&gt;8.57 WBTC-equivalent&lt;/strong&gt; from the &lt;strong&gt;Troves xWBTC/WBTC vault on Starknet&lt;/strong&gt;. Community estimates put the loss at roughly &lt;strong&gt;$660K&lt;/strong&gt;. The incident landed in the same week as the Binance hot wallet theft and got far less attention, but for anyone who builds or audits upgradeable DeFi systems it is arguably the more instructive of the two. Troves says the attack went through &lt;strong&gt;a legacy rebalancing contract that kept its permissions after it had been replaced&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No novel cryptography, no compromised key, no oracle failure. Just an old contract that was supposed to be retired and was still trusted.&lt;/p&gt;

&lt;p&gt;This post walks through what the vault does, what the on-chain transaction shows, why stale permissions are such a common failure, and what teams should change in their deployment and decommissioning process.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol:&lt;/strong&gt; Troves (formerly STRKFarm), a vault-based yield platform on Starknet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affected vault:&lt;/strong&gt; the Ekubo xWBTC/WBTC vault (on-chain name &lt;code&gt;tEkubo xWBTC/WBTC&lt;/code&gt;). Troves says no other vault was affected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain:&lt;/strong&gt; Starknet mainnet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss:&lt;/strong&gt; about 8.57 WBTC-equivalent per Troves' on-chain message; Troves' first public update said about 8.6 BTC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause (per Troves):&lt;/strong&gt; a legacy rebalancing contract retained permissions over the vault after being replaced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response:&lt;/strong&gt; Troves revoked the contract permissions, paused affected and potentially exposed vaults, and offered a 1 WBTC whitehat bounty open until 3 November 2026, 23:59 UTC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Troves Is
&lt;/h2&gt;

&lt;p&gt;Troves is a Starknet yield platform that packages multi-step DeFi strategies into vaults. Users deposit once, and the vault handles lending, liquidity provision and rebalancing for them. According to Troves' own documentation, the vault owners are access control contracts with separate roles for operations such as rebalancing, deposits and withdrawals, and the super admin of that access control is a timelock.&lt;/p&gt;

&lt;p&gt;The affected product was a concentrated liquidity vault that provides liquidity to the &lt;strong&gt;Ekubo&lt;/strong&gt; xWBTC/WBTC pool. &lt;code&gt;xWBTC&lt;/code&gt; is Endur's liquid staked BTC token, and &lt;code&gt;WBTC&lt;/code&gt; is wrapped bitcoin. Because the two assets should trade close to a fixed ratio, the vault concentrates liquidity in a narrow range and needs periodic rebalancing to stay in range. That rebalancing is the job of a privileged contract.&lt;/p&gt;

&lt;p&gt;Rebalancing logic tends to change over time. Teams ship a new strategy, a new router or a new rebalancer, point the vault at it, and move on. The question that matters for this incident is what happens to the old one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened On-Chain
&lt;/h2&gt;

&lt;p&gt;Troves' on-chain message to the attacker names the exploit transaction:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0x0634307777205294f636d481808a7b9a9e208415a26dd19c8ada269b9332e330&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We pulled the receipt from a public Starknet RPC. The transaction was included in &lt;strong&gt;block 17,220,375&lt;/strong&gt; with a block timestamp of &lt;strong&gt;31 October 2026, 08:25:21 UTC&lt;/strong&gt;, and it executed successfully. Its sender is the account &lt;code&gt;0x42dc2ebccae3e55ce3747d2974c801fb3e0facf8d7f4810012a0986a89da5e3&lt;/code&gt;, which made a single call into a helper contract at &lt;code&gt;0x13705ea5f9e591ff6ffe3926bf22fceb19317c049838de09750a60259d38c72&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Reading the token transfer events in order gives a clear outline of the attack, even before Troves publishes a full post-mortem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Flash loan.&lt;/strong&gt; The helper contract receives exactly &lt;strong&gt;10 WBTC&lt;/strong&gt; from &lt;code&gt;0x3976cac2...d24124&lt;/code&gt; at the start of the transaction, and sends exactly 10 WBTC back to the same address near the end. That is the shape of a flash loan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swaps through Ekubo and a routing contract.&lt;/strong&gt; The borrowed WBTC is pushed through the Ekubo core contract via a router that skims a 0.1% fee on each leg, and an Ekubo position NFT is minted to the helper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vault funds move.&lt;/strong&gt; The vault contract (&lt;code&gt;tEkubo xWBTC/WBTC&lt;/code&gt;, &lt;code&gt;0x2ea99b49...4380a&lt;/code&gt;) receives about &lt;strong&gt;9.43 WBTC&lt;/strong&gt; out of Ekubo, which looks like its liquidity being pulled from the position, and then sends about &lt;strong&gt;9.38 WBTC&lt;/strong&gt; to a separate contract at &lt;code&gt;0x2e0af295...184067&lt;/code&gt;, which forwards almost all of it straight back into Ekubo. Shortly after, a similar amount of WBTC leaves Ekubo towards the attacker's route. Depositors' liquidity is being moved and traded inside a transaction the attacker controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit.&lt;/strong&gt; The helper ends up with more WBTC than it borrowed, repays the 10 WBTC loan, and transfers about &lt;strong&gt;6.32 WBTC&lt;/strong&gt; to the attacker's account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We are deliberately careful here. Troves has not yet published which function the legacy contract exposed, or confirmed which address in the trace is the legacy rebalancer, so we do not assert it. The difference between the 6.32 WBTC that reached the sender in this one transaction and the 8.57 WBTC-equivalent Troves reports may come from other assets, other transactions or price conversion, and should be read against Troves' post-mortem when it is released.&lt;/p&gt;

&lt;p&gt;Troves' founder said on X that the exploit was contained within about 40 minutes. Troves' first public statement followed at 14:45 UTC the same day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause: Permissions That Outlived Their Contract
&lt;/h2&gt;

&lt;p&gt;The one-line root cause Troves gave is the most important part of the story: &lt;strong&gt;the legacy rebalancing contract retained permissions after being replaced&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In a role-based setup, a vault trusts certain addresses to call sensitive functions such as "move liquidity", "swap", "withdraw from position" or "deposit into position". When a team upgrades its rebalancing logic by deploying a new contract and granting it the role, the old contract keeps that role unless someone explicitly revokes it. Nothing breaks. Monitoring sees a healthy vault. The new rebalancer works. And the old one sits there, still trusted, running code that nobody is reviewing any more.&lt;/p&gt;

&lt;p&gt;The danger depends on what the old contract exposes. If it has any function that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;can be called by anyone, or by any address that passes a weak check,&lt;/li&gt;
&lt;li&gt;triggers privileged vault actions, and&lt;/li&gt;
&lt;li&gt;lets the caller influence prices, amounts or destinations,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then whoever finds it can make the vault act against its own depositors.&lt;/p&gt;

&lt;p&gt;Here is a &lt;strong&gt;simplified, illustrative&lt;/strong&gt; Solidity sketch of the pattern. It is not Troves' code, which is written in Cairo, and it is not the exploited function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration only. Not the Troves contract.
contract LegacyRebalancer {
    IVault public immutable vault;

    // Originally assumed to be called only by a keeper bot,
    // but never restricted on-chain.
    function rebalance(uint256 amount, bytes calldata swapData) external {
        // The vault trusts this contract via REBALANCER_ROLE.
        vault.withdrawLiquidity(amount);
        _swap(swapData);              // caller-controlled route and limits
        vault.depositLiquidity();
    }
}

// Upgrade day:
// accessControl.grantRole(REBALANCER_ROLE, address(newRebalancer));
// Missing:
// accessControl.revokeRole(REBALANCER_ROLE, address(legacyRebalancer));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;rebalance&lt;/code&gt; lets the caller pick the swap route or the slippage limit, a flash loan is enough to move the pool price, have the vault rebalance at that bad price, and capture the difference. We saw a close relative of this pattern in the &lt;a href="https://www.panda-security.xyz/blog/kolan-hook-exploit-analysis/" rel="noopener noreferrer"&gt;KolanHook Uniswap v4 exploit&lt;/a&gt;, where a permissionless trigger forced a buyback with no meaningful price limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Keeps Happening
&lt;/h2&gt;

&lt;p&gt;Stale permissions are one of the most common findings we see in real audits, and they are one of the hardest to catch with a code review alone, because the bug is not in the code that is in scope. It is in the deployment state.&lt;/p&gt;

&lt;p&gt;A few recurring reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audits are scoped to new code.&lt;/strong&gt; The new rebalancer is reviewed. The old one was reviewed months ago under different assumptions, and nobody re-checks what it can still do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles are additive.&lt;/strong&gt; Granting a role is part of the upgrade script. Revoking the old one is a separate step that is easy to forget, especially if revocation must go through a timelock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Legacy" is not a state on-chain.&lt;/strong&gt; A contract that a team considers retired is, from the chain's point of view, identical to an active one until its rights are removed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Off-chain assumptions decay.&lt;/strong&gt; A function that was "only ever called by our keeper" was safe while the keeper was the only caller that knew about it. That is not access control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We covered a near-identical failure on Avalanche, where a Safe kept &lt;a href="https://www.panda-security.xyz/blog/avalanche-safe-stale-approval-createcall-exploit/" rel="noopener noreferrer"&gt;a stale approval to an address that later had code deployed to it&lt;/a&gt;. The asset and chain were different, the lesson was the same: trust that is never revoked eventually gets used by someone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Builders and Auditors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Treat decommissioning as a deployment
&lt;/h3&gt;

&lt;p&gt;Every upgrade script that grants a role to a new contract should revoke it from the old contract in the same governance action, and tests should assert that the old address no longer holds the role. If the revocation needs a timelock, queue it together with the grant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep an on-chain inventory of privileged addresses
&lt;/h3&gt;

&lt;p&gt;Maintain a list of every address holding each role, generated from chain state rather than from documentation. Review it on every release. Role enumeration is cheap; an unexplained entry is a finding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit the permission graph, not only the contracts
&lt;/h3&gt;

&lt;p&gt;When you scope an audit, include every contract that currently holds a privileged role on the in-scope system, even if it is "old". Our &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt; includes access control and role management items that apply directly here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Never let a caller choose price limits on vault funds
&lt;/h3&gt;

&lt;p&gt;Any path that swaps or rebalances vault liquidity should enforce slippage bounds derived from a trusted price source, not from caller input or the current spot price alone. That single control turns a flash loan attack from profitable into pointless.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make retired contracts inert
&lt;/h3&gt;

&lt;p&gt;Where possible, give contracts an explicit kill switch that a guardian can trip on retirement, so that even if a role is forgotten the code path reverts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitor for unusual privileged calls
&lt;/h3&gt;

&lt;p&gt;Alert on any call into a privileged vault function from a contract other than the current, expected one. In this incident the trigger was a contract the team had already moved away from, which is exactly the kind of caller an allowlist-based alert would flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Addresses and Transactions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exploit tx (Starknet block 17,220,375)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x0634307777205294f636d481808a7b9a9e208415a26dd19c8ada269b9332e330&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attacker account (tx sender)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x42dc2ebccae3e55ce3747d2974c801fb3e0facf8d7f4810012a0986a89da5e3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attacker helper contract&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x13705ea5f9e591ff6ffe3926bf22fceb19317c049838de09750a60259d38c72&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Troves xWBTC/WBTC vault (&lt;code&gt;tEkubo xWBTC/WBTC&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x2ea99b4971d3c277fa4a9b4beb7d4d7d169e683393a29eef263d5d57b4380a&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WBTC on Starknet&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x3fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Troves recovery address (Ethereum)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x91926046e148c66869d71B9Fa37306e82DDc8934&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://x.com/trovesfi/status/2116541963755475375" rel="noopener noreferrer"&gt;Troves on X: incident statement&lt;/a&gt;: loss of about 8.6 BTC, legacy rebalancing contract, permissions revoked, vaults paused&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/DefimonAlerts/status/2116543245211587053" rel="noopener noreferrer"&gt;Defimon Alerts on X: Troves on-chain message to the attacker&lt;/a&gt;: 8.57 WBTC-equivalent, exploit transaction hash, 1 WBTC bounty and deadline&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/akiraonstarknet/status/2116596840078647710" rel="noopener noreferrer"&gt;Troves founder on X&lt;/a&gt;: containment within about 40 minutes&lt;/li&gt;
&lt;li&gt;&lt;a href="https://telem.news/security/troves-xwbtc-wbtc-vault-starknet-exploited-8-57-wbtc-fcebd9f6" rel="noopener noreferrer"&gt;Telem News: Troves xWBTC/WBTC vault exploited for 8.57 WBTC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.troves.fi/p/strategies/vesu-fusion-rebalancing-vaults" rel="noopener noreferrer"&gt;Troves docs: Vesu Fusion rebalancing vaults&lt;/a&gt;: role-based access control and timelocked super admin&lt;/li&gt;
&lt;li&gt;Starknet transaction receipt for &lt;code&gt;0x0634...e330&lt;/code&gt;, read from a public Starknet RPC node: block number, timestamp and transfer events&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The Troves incident is small next to this week's headline losses, but its root cause is one almost every protocol carries in some form: a contract the team stopped thinking about that the chain still trusts. Upgrades are not finished when the new code works; they are finished when the old code can no longer do anything. If you are planning an upgrade, a migration or a vault launch, &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;talk to our team&lt;/a&gt; or check our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt; to have the full permission graph reviewed, not just the new files.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>starknet</category>
      <category>web3</category>
    </item>
    <item>
      <title>Binance Hack: How EIP-7702 Delegation Emptied a Hot Wallet</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Fri, 30 Oct 2026 23:55:53 +0000</pubDate>
      <link>https://dev.to/pandasecurity/binance-hack-how-eip-7702-delegation-emptied-a-hot-wallet-6g5</link>
      <guid>https://dev.to/pandasecurity/binance-hack-how-eip-7702-delegation-emptied-a-hot-wallet-6g5</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/binance-eip-7702-hot-wallet-delegation-exploit/" rel="noopener noreferrer"&gt;Binance Hack: How EIP-7702 Delegation Emptied a Hot Wallet&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;29 October 2026&lt;/strong&gt;, hot wallets belonging to &lt;strong&gt;Binance&lt;/strong&gt; paid out to an attacker across several blockchains within roughly half an hour. Binance puts the total impact at &lt;strong&gt;about $6.3 billion&lt;/strong&gt;; Chainalysis counts &lt;strong&gt;about $6.4 billion&lt;/strong&gt;, which would make it the largest theft in crypto history. Most legs of the attack were ordinary signed transfers. The Ethereum leg was different: the attacker used &lt;strong&gt;EIP-7702&lt;/strong&gt; to make one of Binance's busiest Ethereum hot wallets, labelled &lt;em&gt;Binance 14&lt;/em&gt; on Etherscan, run the attacker's own contract code. Less than four minutes later, about &lt;strong&gt;167,464 ETH and 63 tokens&lt;/strong&gt; left that wallet in a single transaction.&lt;/p&gt;

&lt;p&gt;No smart contract bug was exploited and no signature was forged. That is exactly why this incident matters to anyone who builds wallets, custody systems or contracts that interact with EOAs. This post walks through what happened on-chain, why EIP-7702 changes the blast radius of a signing compromise, and what to take away from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Victim:&lt;/strong&gt; Binance hot wallets on multiple chains; on Ethereum, &lt;em&gt;Binance 14&lt;/em&gt; (&lt;code&gt;0x28C6c062...1d60&lt;/code&gt;) was delegated, while &lt;em&gt;Binance 16&lt;/em&gt; sent plain transfers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total impact:&lt;/strong&gt; about $6.3B according to Binance, about $6.4B according to Chainalysis. Around $3B has been frozen by stablecoin issuers and partners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ethereum mechanism:&lt;/strong&gt; one type-4 transaction carried &lt;strong&gt;50 valid EIP-7702 authorizations&lt;/strong&gt;, all signed by Binance 14, all pointing the wallet at an attacker-deployed sweeper contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause (per Binance's initial investigation):&lt;/strong&gt; a compromised DevOps engineer's laptop gave the attacker access to a service container that could submit withdrawal transactions. Binance says it found no evidence of a private key leak, and co-CEO Richard Teng later said the delegation did not pass through the exchange's normal approval controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; the malicious delegation stayed active for nearly four hours, until Binance 14 cleared it at 00:10:35 UTC on 30 October.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Quick Refresher on EIP-7702
&lt;/h2&gt;

&lt;p&gt;EIP-7702 went live on Ethereum mainnet with the &lt;strong&gt;Pectra&lt;/strong&gt; upgrade in May 2025. It adds a new transaction type (type 4, "set code") with an &lt;code&gt;authorizationList&lt;/code&gt;. Each entry is a tuple of &lt;code&gt;(chainId, address, nonce)&lt;/code&gt; signed by an EOA. When the transaction is processed, the EOA's code is set to a small delegation designator, &lt;code&gt;0xef0100&lt;/code&gt; followed by the target address. From then on, any call to the EOA executes the target contract's code &lt;strong&gt;in the context of the EOA&lt;/strong&gt;: its balance, its storage, its token holdings and every permission it holds elsewhere.&lt;/p&gt;

&lt;p&gt;Three properties are important here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Anyone can submit the authorization.&lt;/strong&gt; The signer does not need to send the type-4 transaction itself. A third party can carry a signed tuple on-chain and pay the gas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The authorization is bound to the account nonce.&lt;/strong&gt; A tuple only applies if its &lt;code&gt;nonce&lt;/code&gt; equals the authority's current nonce at processing time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The authorization carries no amount and no recipient.&lt;/strong&gt; It does not say "send X to Y". It says "run this code", which can mean "send everything to whoever asks".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In other words, signing a 7702 authorization is closer to handing over the account than to approving a payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened on Ethereum
&lt;/h2&gt;

&lt;p&gt;All times below are block timestamps in UTC. Transaction details were checked against an Ethereum RPC node and matched against independent write-ups listed in the Sources section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preparation: Tooling Deployed Hours Earlier
&lt;/h3&gt;

&lt;p&gt;The attacker's operator account, &lt;code&gt;0x7c8601d4...5ca0&lt;/code&gt;, was reportedly funded through Tornado Cash. On-chain analyses by Tibane Labs and an IMD incident report show it deploying a first sweeper version early on 29 October and testing it on fresh EOAs it controlled. At about &lt;strong&gt;16:00 UTC&lt;/strong&gt; it deployed the sweeper used against Binance, &lt;code&gt;0x63e842b0...6fae&lt;/code&gt;, and ran a final test delegation and sweep on another of its own wallets. In other words, the tooling was rehearsed and ready hours before the attack.&lt;/p&gt;

&lt;p&gt;According to Tibane Labs' decompilation, every function in the sweeper checks that the caller is the hard-coded operator address. Its main entry points are a &lt;code&gt;sweep(to, tokens[])&lt;/code&gt; function that sends all ETH and the full balance of each listed token, and an &lt;code&gt;execute(target, value, data)&lt;/code&gt; function that makes an arbitrary call.&lt;/p&gt;

&lt;h3&gt;
  
  
  20:20:59: Fifty Authorizations in One Transaction
&lt;/h3&gt;

&lt;p&gt;At block &lt;strong&gt;26,300,753&lt;/strong&gt;, the operator sent type-4 transaction &lt;code&gt;0xad99264d...298b&lt;/code&gt; to Binance 14. Its authorization list contains &lt;strong&gt;50 tuples&lt;/strong&gt;, all delegating to the sweeper &lt;code&gt;0x63e842b0...6fae&lt;/code&gt;, with consecutive nonces from &lt;strong&gt;18,914,775 to 18,914,824&lt;/strong&gt;. Independent analysts recovered the signer of every tuple and found that all 50 were signed by Binance 14's own key.&lt;/p&gt;

&lt;p&gt;Why fifty? Binance 14 is an extremely busy withdrawal wallet that sends transactions almost every block, so its nonce never sits still. A single authorization would very likely be stale by the time it landed. By obtaining signatures for a whole window of upcoming nonces, the attacker made sure exactly one of them would match whatever nonce the wallet had when the transaction was included. The rest were simply skipped as invalid, which EIP-7702 allows without reverting the transaction.&lt;/p&gt;

&lt;p&gt;Tibane Labs notes that Binance 14 itself used nonce 18,914,775 about three minutes before the delegation landed. Nobody signs an authorization for a nonce that is already spent, so the batch appears to have been produced live, on demand, rather than taken from an old leak.&lt;/p&gt;

&lt;h3&gt;
  
  
  20:24:23: One Call Sweeps the Wallet
&lt;/h3&gt;

&lt;p&gt;At block &lt;strong&gt;26,300,770&lt;/strong&gt;, the operator called Binance 14 directly with transaction &lt;code&gt;0x53bd38b8...e2f3&lt;/code&gt;. Because Binance 14 was now delegated, the call ran the sweeper's &lt;code&gt;sweep&lt;/code&gt; function (selector &lt;code&gt;0x8a179be4&lt;/code&gt;) as Binance 14. The recipient argument in the calldata is &lt;code&gt;0x5227f603...2f8c&lt;/code&gt;. In that single transaction, about &lt;strong&gt;167,464 ETH and 63 tokens&lt;/strong&gt;, including roughly 1.6 billion USDT according to CoinDesk, moved to the attacker.&lt;/p&gt;

&lt;p&gt;Twenty-four seconds later, &lt;strong&gt;Binance 16&lt;/strong&gt; sent the same attacker address &lt;strong&gt;28,005.6 ETH&lt;/strong&gt; and several tokens through ordinary signed transfers, with no delegation involved. Together, the two wallets lost roughly 200,000 ETH.&lt;/p&gt;

&lt;h3&gt;
  
  
  The execute() Path: More Than a Balance
&lt;/h3&gt;

&lt;p&gt;A delegated EOA does not only expose its own balance. It exposes every role, allowance and admin right that address holds. According to TRM Labs (via CoinDesk) and Tibane Labs, Binance 14 also had control over a separate Binance smart-contract wallet holding USDC, an EIP-1967 proxy. Through the sweeper's &lt;code&gt;execute&lt;/code&gt; function, the attacker made Binance 14 instruct that contract to release &lt;strong&gt;823 million USDC&lt;/strong&gt; (780M and 43M in two calls) to the attacker. Circle later froze those funds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nearly Four Hours of Live Delegation
&lt;/h3&gt;

&lt;p&gt;The delegation did not expire by itself. Binance's normal systems kept routing deposits and rebalancing flows into Binance 14, and the attacker swept them again at around 21:04 and 21:44 UTC, according to CoinDesk. Only at &lt;strong&gt;00:10:35 UTC on 30 October&lt;/strong&gt; (block 26,301,896) did Binance 14 send itself a type-4 transaction, &lt;code&gt;0xafbf94c9...7209&lt;/code&gt;, with a single authorization to the &lt;strong&gt;zero address&lt;/strong&gt;. That clears the delegation designator, and the wallet's code returned to empty.&lt;/p&gt;

&lt;p&gt;Clearing the code removes the attacker's program, but it does not rotate the key. Anyone who can still sign for the account at its current nonce could install a new delegation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause: A Signing Compromise, Amplified by 7702
&lt;/h2&gt;

&lt;p&gt;It is tempting to call this an "EIP-7702 exploit", but the chain data does not show a flaw in EIP-7702 itself. Binance's announcement states that the incident may involve a compromised DevOps engineer's laptop that let the attacker reach a &lt;strong&gt;service container&lt;/strong&gt; and submit unauthorized withdrawal transactions, and that it found no evidence of a wallet private key leak. In a later video update, Teng said investigators had found additional points of breach and that the EIP-7702 delegation &lt;strong&gt;did not pass through Binance's normal approval controls&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That combination explains the on-chain picture. Hot wallets on many chains signed for the attacker within the same half hour, some with plain transfers and one with a batch of 7702 authorizations. The common factor is the signing layer. A service that is trusted to produce signatures will sign whatever reaches it in a shape it accepts.&lt;/p&gt;

&lt;p&gt;What 7702 changed is the &lt;strong&gt;shape&lt;/strong&gt; of what could be signed. A withdrawal policy that inspects recipients and amounts has nothing to inspect in an authorization tuple: there is no recipient and no amount, only a code address. If the signing pipeline treated a type-4 authorization as just another message, the controls built for transfers never had a chance to fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simplified Version of the Pattern
&lt;/h2&gt;

&lt;p&gt;The snippet below is &lt;strong&gt;simplified and illustrative&lt;/strong&gt;. It is not the attacker's bytecode; it shows why a delegated EOA is fully exposed once an attacker-controlled contract becomes its code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SIMPLIFIED, illustrative only
interface IERC20 {
    function balanceOf(address) external view returns (uint256);
    function transfer(address, uint256) external returns (bool);
}

contract Sweeper {
    address immutable operator; // stored in code, not in the EOA's storage

    constructor(address op) { operator = op; }

    // Runs with address(this) == the delegated EOA
    function sweep(address to, address[] calldata tokens) external {
        require(msg.sender == operator, "not operator");
        for (uint256 i; i &amp;lt; tokens.length; i++) {
            if (tokens[i] == address(0)) {
                payable(to).transfer(address(this).balance);
            } else {
                IERC20 t = IERC20(tokens[i]);
                t.transfer(to, t.balanceOf(address(this)));
            }
        }
    }

    // Anything the EOA is allowed to do elsewhere, the operator can now do
    function execute(address target, uint256 value, bytes calldata data) external {
        require(msg.sender == operator, "not operator");
        (bool ok, ) = target.call{value: value}(data);
        require(ok, "call failed");
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key line is the comment on &lt;code&gt;sweep&lt;/code&gt;: once delegated, &lt;code&gt;address(this)&lt;/code&gt; is the victim wallet, so &lt;code&gt;balanceOf(address(this))&lt;/code&gt; and &lt;code&gt;msg.sender&lt;/code&gt; checks in downstream contracts all see the victim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Builders and Auditors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For Custody and Signing Infrastructure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Treat 7702 authorizations as a separate, highest-risk signing class.&lt;/strong&gt; They should never share a code path with withdrawals. If a hot wallet has no business delegating, the signer should refuse type-4 authorizations outright.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowlist delegation targets.&lt;/strong&gt; If delegation is needed, only sign tuples whose &lt;code&gt;address&lt;/code&gt; is a known, audited implementation, and require multi-party approval for that list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for nonce windows.&lt;/strong&gt; A request to sign many authorizations for consecutive future nonces is a strong anomaly signal on its own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor code on hot wallets.&lt;/strong&gt; An alert on &lt;code&gt;eth_getCode(hotWallet) != 0x&lt;/code&gt; would have flagged this within one block instead of nearly four hours later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop routing funds into a wallet you are investigating.&lt;/strong&gt; Automated deposit and rebalancing flows kept feeding Binance 14 while it was compromised.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  For Smart Contract Developers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do not assume an EOA is "just a key".&lt;/strong&gt; After Pectra, any EOA can carry code. Checks such as &lt;code&gt;tx.origin == msg.sender&lt;/code&gt; or &lt;code&gt;extcodesize == 0&lt;/code&gt; no longer prove a caller is a simple wallet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimise privileges held by hot EOAs.&lt;/strong&gt; Binance 14's control over a separate USDC wallet turned one compromise into a second, $823M loss. Admin roles and large allowances belong to cold, multi-signature accounts, not to withdrawal wallets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review delegation targets like any other contract.&lt;/strong&gt; If your product asks users to sign 7702 authorizations, the delegate contract is now part of your attack surface and deserves a full audit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We covered related signing and approval failures in our analyses of the &lt;a href="https://www.panda-security.xyz/blog/argent-style-wallet-recovery-replay-exploit/" rel="noopener noreferrer"&gt;Argent-style wallet recovery replay&lt;/a&gt; and the &lt;a href="https://www.panda-security.xyz/blog/avalanche-safe-stale-approval-createcall-exploit/" rel="noopener noreferrer"&gt;Avalanche Safe stale approval exploit&lt;/a&gt;. Our &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt; includes items on privileged roles and caller assumptions that apply directly here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Response and Current Status
&lt;/h2&gt;

&lt;p&gt;Binance suspended withdrawals, revoked the affected address authorization, engaged law enforcement, and says it will cover all losses from its SAFU fund and company funds. Teng said Binance has reserved more than $6.34 billion and aims to reopen withdrawals by Sunday, 1 November, with a full proof-of-reserves report targeted for Monday, 2 November. Tether, Circle and other issuers froze a large share of the stolen stablecoins. Chainalysis and TRM Labs attribute the theft to the North Korea-linked group TraderTraitor; Binance has not published a final post-mortem yet, so details of the initial access may still change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Addresses and Transactions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Binance 14 (victim, delegated)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x28C6c06298d514Db089934071355E5743bf21d60&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attacker operator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x7c8601d4645b93cea45f71e118a729332e6b5ca0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sweeper contract&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x63e842b054bd624a465454d533c33f573e9c6fae&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attacker receiving address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x5227f603c1ee34ce3c06efe6477003bdfccb2f8c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delegation tx (block 26,300,753)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xad99264d5532ae0dd04b2004eebda05f25657e70dacb764f7ac8d4ccd051298b&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First sweep tx (block 26,300,770)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x53bd38b816c23e74134786c29a8c18d792777998a3b9326047b583100e88e2f3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revocation tx (block 26,301,896)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xafbf94c92a8d749988411669fa14d4f8866d9b4420b4ebd299f28e958dac7209&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.binance.com/en/support/announcement/detail/c5a8729a97ac486d963784d96c5642d7" rel="noopener noreferrer"&gt;Binance: Security Incident Update (2026-10-30)&lt;/a&gt;: initial findings, impact estimate, response&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.coindesk.com/tech/2026/10/30/here-s-how-one-ethereum-permission-let-an-attacker-drain-usd2-5-billion-from-binance" rel="noopener noreferrer"&gt;CoinDesk: How one Ethereum permission let an attacker drain $2.5 billion from Binance&lt;/a&gt;: timeline, nonce window, amounts&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.coindesk.com/tech/2026/10/30/binance-uncovers-wider-breach-in-usd6-3-billion-exploit-targets-sunday-withdrawal-restart" rel="noopener noreferrer"&gt;CoinDesk: Binance discovers multiple breach points&lt;/a&gt;: Teng's update on approval controls&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://decrypt.co/382237/binance-sets-aside-6-34b-to-cover-hack-losses-ceo-says-your-assets-are-safe" rel="noopener noreferrer"&gt;Decrypt: Binance sets aside $6.34B to cover hack losses&lt;/a&gt;: reimbursement and attribution&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.chainalysis.com/blog/binance-exploit-latest-findings-and-response/" rel="noopener noreferrer"&gt;Chainalysis: Binance hack findings&lt;/a&gt;: total, freezes and attribution&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.tibane.net/research/binance-hack" rel="noopener noreferrer"&gt;Tibane Labs: Binance hack on-chain analysis&lt;/a&gt;: signer recovery, sweeper functions, multi-chain timeline&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.maborak.com/blog/binance-14-eip-7702-revocation" rel="noopener noreferrer"&gt;Maborak: Binance 14's EIP-7702 revocation, checked on-chain&lt;/a&gt;: revocation transaction&lt;/li&gt;
&lt;li&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-7702" rel="noopener noreferrer"&gt;EIP-7702 specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The Binance theft is a reminder that the most powerful thing a key can sign is no longer a transfer. Since Pectra, one signature can replace an account's behaviour entirely, and every system that signs on behalf of an EOA needs controls that understand that. If you are building custody flows, smart accounts or contracts that rely on caller assumptions, &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;talk to our team&lt;/a&gt; or check our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt; to get a review before an attacker does it for you.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>security</category>
      <category>ethereum</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>zkLink Nova Exploit: A Stale Transient Hash Forged a Root</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Thu, 29 Oct 2026 23:05:39 +0000</pubDate>
      <link>https://dev.to/pandasecurity/zklink-nova-exploit-a-stale-transient-hash-forged-a-root-oji</link>
      <guid>https://dev.to/pandasecurity/zklink-nova-exploit-a-stale-transient-hash-forged-a-root-oji</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/zklink-nova-arbitrator-forged-root-exploit/" rel="noopener noreferrer"&gt;zkLink Nova Exploit: A Stale Transient Hash Forged a Root&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;22:21:35 UTC on 28 October 2026&lt;/strong&gt; (Ethereum block 26,294,188), a single transaction emptied the Ethereum escrows of &lt;strong&gt;zkLink Nova&lt;/strong&gt;, an aggregated rollup. The attacker walked away with &lt;strong&gt;22.43 ETH plus 11 ERC-20 tokens&lt;/strong&gt;, led by about &lt;strong&gt;245.74 pufETH&lt;/strong&gt;. Security firms priced the haul at roughly &lt;strong&gt;$743K to $805K&lt;/strong&gt; depending on the pricing source; BlockSec Phalcon put it at about $780K and SlowMist at about $783.1K.&lt;/p&gt;

&lt;p&gt;No key was stolen and no signature was forged. The attacker wrote their own Merkle root into the bridge and then withdrew against proofs they had built themselves. Two small design decisions in one contract made that possible: a transient-storage hash that was never cleared, and a privileged function that called whatever target its caller supplied.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol:&lt;/strong&gt; zkLink Nova, an aggregated rollup with escrows on Ethereum and several other chains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain:&lt;/strong&gt; Ethereum mainnet. Follow-on attacks were reported on other chains (see below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss on Ethereum:&lt;/strong&gt; 22.43 ETH and 11 ERC-20 tokens, roughly $780K by most estimates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;Arbitrator.claimMessage()&lt;/code&gt; checked a transient &lt;code&gt;finalizeMessageHash&lt;/code&gt; that it never reset, and it executed a caller-supplied target and calldata under the Arbitrator's own trusted identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entry point:&lt;/strong&gt; &lt;code&gt;SyncL2TxHashRelayer&lt;/code&gt;, a whitelisted relayer contract that anyone can call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response:&lt;/strong&gt; zkLink suspended block production on Nova, emergency-patched the affected contracts and temporarily disabled withdrawals.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How zkLink Nova Moves Batch Roots
&lt;/h2&gt;

&lt;p&gt;Each zkLink Nova escrow stores Merkle roots of L2-to-L1 message logs, one per batch. A withdrawal on L1 is valid if its Merkle proof matches the stored root for that batch. In other words, whoever can write a root can approve any withdrawal they like.&lt;/p&gt;

&lt;p&gt;Roots do not arrive directly. They travel through a relay chain that looks like this on Ethereum:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A relayer calls the &lt;strong&gt;Arbitrator&lt;/strong&gt;, a cross-chain message router gated by &lt;code&gt;onlyRelayer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Arbitrator claims a message from a source chain's canonical message service (for Nova, Linea's &lt;code&gt;L1MessageService&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;During that claim, the source chain's gateway calls back into &lt;code&gt;Arbitrator.receiveMessage()&lt;/code&gt;, which records a hash of what was genuinely delivered.&lt;/li&gt;
&lt;li&gt;The Arbitrator checks that hash and forwards the message to destination gateways, such as the &lt;strong&gt;EthereumGateway&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The EthereumGateway, gated by &lt;code&gt;onlyArbitrator&lt;/code&gt;, calls &lt;code&gt;ZkLink.syncBatchRoot()&lt;/code&gt;, gated by &lt;code&gt;onlyGateway&lt;/code&gt;, which writes the root.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every hop trusts the hop before it purely by &lt;code&gt;msg.sender&lt;/code&gt;. That chain of trust is only as strong as the Arbitrator's guarantee that it forwards nothing it did not genuinely receive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause: Two Flaws in claimMessage
&lt;/h2&gt;

&lt;p&gt;The relevant code is public in zkLink's &lt;code&gt;zklink-evm-contracts&lt;/code&gt; repository. The function signature makes the first problem visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function claimMessage(
    address _sourceChainCanonicalMessageService, // chosen by the relayer
    bytes calldata _sourceChainClaimCallData,    // chosen by the relayer
    IL1Gateway _sourceChainL1Gateway,
    uint256 _receiveValue,
    bytes calldata _receiveCallData,
    bytes calldata _forwardParams
) external payable nonReentrant onlyRelayer whenNotPaused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Flaw 1: An Arbitrary Call With a Trusted Identity
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;claimMessage&lt;/code&gt; performs &lt;code&gt;Address.functionCall(_sourceChainCanonicalMessageService, _sourceChainClaimCallData)&lt;/code&gt;. Both the target and the calldata come from the caller. Because the call originates from the Arbitrator, the callee sees &lt;code&gt;msg.sender == Arbitrator&lt;/code&gt;, which is exactly the identity that every gateway's &lt;code&gt;onlyArbitrator&lt;/code&gt; modifier trusts.&lt;/p&gt;

&lt;p&gt;So if the "canonical message service" is set to the &lt;strong&gt;EthereumGateway&lt;/strong&gt; itself, and the calldata is &lt;code&gt;sendMessage(...)&lt;/code&gt; wrapping &lt;code&gt;syncBatchRoot(...)&lt;/code&gt;, both access checks pass. The gateway believes the Arbitrator asked it to forward a root, and ZkLink believes its gateway delivered one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flaw 2: A Validation Hash That Outlives Its Claim
&lt;/h3&gt;

&lt;p&gt;The only thing standing between that arbitrary call and a forged root is a check that runs after it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bytes32 _finalizeMessageHash;
assembly { _finalizeMessageHash := tload(finalizeMessageHash.slot) }
require(
    _finalizeMessageHash ==
        keccak256(abi.encode(_sourceChainL1Gateway, _receiveValue, _receiveCallData)),
    "Incorrect finalize data"
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;finalizeMessageHash&lt;/code&gt; lives in &lt;strong&gt;EIP-1153 transient storage&lt;/strong&gt;. It is written by &lt;code&gt;receiveMessage()&lt;/code&gt; when a registered gateway delivers a real message. Transient storage is scoped to the whole transaction, not to a single call. The Arbitrator never reset the slot after the check.&lt;/p&gt;

&lt;p&gt;That turns the check into a much weaker statement. It no longer says "a genuine delivery happened during this claim". It says "a genuine delivery with these parameters happened at some point earlier in this transaction". Once one real claim arms the slot, any later claim in the same transaction that repeats the same gateway, value and calldata passes, even if its external call delivered nothing.&lt;/p&gt;

&lt;p&gt;Note that &lt;code&gt;nonReentrant&lt;/code&gt; does not help here. The two claims are sequential calls from the attacker's contract, not nested ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack, Step by Step
&lt;/h2&gt;

&lt;p&gt;BlockSec Phalcon and SlowMist describe the same sequence, and the token transfers we read from the transaction receipt match it. All steps ran inside one transaction sent from the attacker EOA to a purpose-built attack contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Arm the Hash With a Genuine Message
&lt;/h3&gt;

&lt;p&gt;The attacker called the public &lt;code&gt;SyncL2TxHashRelayer&lt;/code&gt; to claim a &lt;strong&gt;real, pending Linea message&lt;/strong&gt; that synchronised two L2 transaction hashes. The Linea bridge verified the proof, the Linea gateway called back into the Arbitrator, and &lt;code&gt;finalizeMessageHash&lt;/code&gt; was set to the hash of that genuine message. The legitimate message was forwarded as normal. The slot stayed armed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Smuggle a Root Update Through the Same Function
&lt;/h3&gt;

&lt;p&gt;The attacker called the relayer a second time in the same transaction. This time the "canonical message service" was the EthereumGateway, and the claim calldata was a &lt;code&gt;sendMessage&lt;/code&gt; call wrapping &lt;code&gt;syncBatchRoot(116409, attackerChosenRoot, 0)&lt;/code&gt;. The gateway accepted it because the caller was the Arbitrator, and ZkLink stored the attacker's root for batch &lt;strong&gt;116409&lt;/strong&gt; because the caller was its gateway.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Pass Validation With the Stale Hash
&lt;/h3&gt;

&lt;p&gt;The EthereumGateway never calls back into &lt;code&gt;receiveMessage&lt;/code&gt;, so nothing overwrote the slot. The attacker supplied the same gateway, value and calldata as in Step 1, so the recomputed hash matched the stale one. The forged root survived.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Withdraw Against the Forged Root
&lt;/h3&gt;

&lt;p&gt;Finalising a withdrawal on zkLink is permissionless and relies only on the stored root. The attacker submitted &lt;strong&gt;12 withdrawal proofs&lt;/strong&gt; built against their own root: one ETH withdrawal from the ZkLink escrow and eleven token withdrawals from the &lt;code&gt;L1ERC20Bridge&lt;/code&gt;. Every receiver was the attacker's address.&lt;/p&gt;

&lt;p&gt;From the transaction receipt we read the following transfers out of the &lt;code&gt;L1ERC20Bridge&lt;/code&gt; to the attacker EOA:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pufETH&lt;/td&gt;
&lt;td&gt;245.7379&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USDC&lt;/td&gt;
&lt;td&gt;18,113.45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ezETH&lt;/td&gt;
&lt;td&gt;12.5693&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rsETH&lt;/td&gt;
&lt;td&gt;6.8665&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STONE&lt;/td&gt;
&lt;td&gt;6.2998&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USDT&lt;/td&gt;
&lt;td&gt;3,360.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mstETH&lt;/td&gt;
&lt;td&gt;2.6372&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mswETH&lt;/td&gt;
&lt;td&gt;2.4826&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rswETH&lt;/td&gt;
&lt;td&gt;0.5000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wstETH&lt;/td&gt;
&lt;td&gt;0.4660&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WBTC&lt;/td&gt;
&lt;td&gt;0.1736&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On top of these, &lt;strong&gt;22.43 ETH&lt;/strong&gt; came out of the ZkLink escrow itself. Liquid restaking tokens made up most of the value, with pufETH alone accounting for the bulk of the loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simplified Version of the Bug
&lt;/h2&gt;

&lt;p&gt;The snippet below is a &lt;strong&gt;simplified, illustrative&lt;/strong&gt; model of the pattern, not zkLink's production code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration only
contract Router {
    bytes32 transient lastDelivered; // armed by trusted callbacks

    function onDelivered(bytes32 h) external onlyGateway {
        lastDelivered = h;
    }

    function claim(address target, bytes calldata data, bytes32 expected) external {
        (bool ok, ) = target.call(data);     // caller picks target and data
        require(ok);
        require(lastDelivered == expected);  // stale value still passes
        // BUG: lastDelivered is never cleared
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safer shape pins the target and consumes the evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration only
function claim(bytes calldata data, bytes32 expected) external {
    (bool ok, ) = canonicalService[msg.sender].call(data); // fixed target
    require(ok);
    require(lastDelivered == expected);
    lastDelivered = bytes32(0);                            // one claim, one use
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parapet Labs, which replayed the attack on a mainnet fork, reports that &lt;strong&gt;either change alone&lt;/strong&gt; makes the forged claim revert, and that both belong in a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow-On Attacks and Response
&lt;/h2&gt;

&lt;p&gt;BlockSec Phalcon later reported similar attack transactions against zkLink Nova on several L2 chains, and zkLink's own statement at 09:06 UTC on 29 October referred to an ongoing exploit of the Arbitrator on &lt;strong&gt;Ethereum and Optimism&lt;/strong&gt;. Parapet Labs attributes this second wave, at about 07:16 UTC on 29 October, to a different actor and reports roughly 24.76 ETH plus token balances taken from escrows on Optimism, Base, Blast and Mantle. We have not independently verified those second-wave figures.&lt;/p&gt;

&lt;p&gt;zkLink offered a &lt;strong&gt;20% white-hat bounty&lt;/strong&gt; for returned funds. At 13:08 UTC on 29 October it said block production on Nova had been suspended, the affected contracts emergency-patched and withdrawals temporarily disabled. Parapet also notes that pauses applied to secondary chains in 2025 blocked deposits but not withdrawal finalisation, which is why those paused escrows could still be drained.&lt;/p&gt;

&lt;p&gt;Cross-chain message routers keep failing in similar ways. Our analyses of the &lt;a href="https://www.panda-security.xyz/blog/initia-movevm-script-cache-bridge-exploit/" rel="noopener noreferrer"&gt;Initia MoveVM bridge exploit&lt;/a&gt; and the &lt;a href="https://www.panda-security.xyz/blog/wax-bridge-negative-fee-exploit/" rel="noopener noreferrer"&gt;WAX bridge negative fee exploit&lt;/a&gt; show the same theme: a bridge is only as safe as the weakest check on what it is told to release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Builders and Auditors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Consume transient flags after use.&lt;/strong&gt; EIP-1153 slots persist for the whole transaction. If a value represents "this specific claim was authenticated", clear it with &lt;code&gt;tstore(slot, 0)&lt;/code&gt; immediately after the check. Do the same for session flags such as &lt;code&gt;claiming&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Never make arbitrary calls from a privileged identity.&lt;/strong&gt; If other contracts authorise by &lt;code&gt;msg.sender == address(this)&lt;/code&gt;, then any caller-chosen target and calldata is a privilege escalation path. Pin the canonical service per gateway or keep an explicit allowlist of call targets and selectors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Check before you act, not after.&lt;/strong&gt; Here the privileged call ran first and validation came second. Ordering validation before effects is the same discipline that prevents reentrancy, and it removes whole classes of bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Treat permissionless relayers as public entry points.&lt;/strong&gt; An &lt;code&gt;onlyRelayer&lt;/code&gt; modifier means little if one of the whitelisted relayers forwards any caller's arguments unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Make pauses cover withdrawals.&lt;/strong&gt; An emergency stop that blocks deposits but leaves &lt;code&gt;finalizeWithdrawal&lt;/code&gt; open does not protect escrowed funds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Test multi-call transactions.&lt;/strong&gt; Fuzz harnesses should call the same entry point several times within one transaction with mixed genuine and malicious inputs. Single-call unit tests will never catch a stale transient value. Our &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt; covers these test patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Addresses and Transactions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Exploit transaction: &lt;a href="https://etherscan.io/tx/0x4f3c23e75b9134e8cae5a462550daad42fbbc19242567fcc34c0a035e297462c" rel="noopener noreferrer"&gt;&lt;code&gt;0x4f3c23e7...297462c&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Related transaction listed by SlowMist: &lt;a href="https://etherscan.io/tx/0x8a7e38b9c2a7a40981317ba87fbaa2e50527ec412cefc2fb4c9e3184f8848ae9" rel="noopener noreferrer"&gt;&lt;code&gt;0x8a7e38b9...f8848ae9&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Attacker EOA: &lt;a href="https://etherscan.io/address/0xaca0655a11200a80d33d0e48786317304cde5718" rel="noopener noreferrer"&gt;&lt;code&gt;0xaca0655a11200a80d33d0e48786317304cde5718&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Attack contract: &lt;a href="https://etherscan.io/address/0xd5f162b68a0281686e8dc76db8d67d2f3e6aff07" rel="noopener noreferrer"&gt;&lt;code&gt;0xd5f162b68a0281686e8dc76db8d67d2f3e6aff07&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Second address listed by SlowMist: &lt;a href="https://etherscan.io/address/0xdab137f390df77caa97fd492540e973fd5e2f8c8" rel="noopener noreferrer"&gt;&lt;code&gt;0xdab137f390df77caa97fd492540e973fd5e2f8c8&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Arbitrator (proxy): &lt;a href="https://etherscan.io/address/0x1ee09a2caa0813a5183f90f5a6d0e4871f4c6002" rel="noopener noreferrer"&gt;&lt;code&gt;0x1ee09a2caa0813a5183f90f5a6d0e4871f4c6002&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ZkLink escrow: &lt;a href="https://etherscan.io/address/0x5fd9f73286b7e8683bab45019c94553b93e015cf" rel="noopener noreferrer"&gt;&lt;code&gt;0x5fd9f73286b7e8683bab45019c94553b93e015cf&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;L1ERC20Bridge: &lt;a href="https://etherscan.io/address/0xad16edcf7deb7e90096a259c81269d811544b6b6" rel="noopener noreferrer"&gt;&lt;code&gt;0xad16edcf7deb7e90096a259c81269d811544b6b6&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://x.com/Phalcon_xyz/status/2115671779717083260" rel="noopener noreferrer"&gt;BlockSec Phalcon alert on X&lt;/a&gt;: root cause, five-step breakdown, batch number and asset amounts&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/SlowMist_Team/status/2115635114638250409" rel="noopener noreferrer"&gt;SlowMist TI Alert on X&lt;/a&gt;: root cause, attacker, victim and vulnerable contract addresses, transactions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/DefimonAlerts/status/2115672412486344960" rel="noopener noreferrer"&gt;Defimon alert on X&lt;/a&gt;: per-asset breakdown and loss estimate&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/zkLink_Official/status/2115731883313254565" rel="noopener noreferrer"&gt;zkLink statement on X&lt;/a&gt; and &lt;a href="https://x.com/zkLink_Official/status/2115792847098216545" rel="noopener noreferrer"&gt;follow-up&lt;/a&gt;: confirmation, bounty offer and emergency measures&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://parapetlabs.net/blog/zklink-nova-exploit-post-mortem/" rel="noopener noreferrer"&gt;Parapet Labs post-mortem&lt;/a&gt;: fork replay, fix isolation and second-wave tracing&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/zkLinkProtocol/zklink-evm-contracts/blob/main/contracts/Arbitrator.sol" rel="noopener noreferrer"&gt;zkLink Arbitrator.sol on GitHub&lt;/a&gt;: &lt;code&gt;claimMessage&lt;/code&gt; source&lt;/li&gt;
&lt;li&gt;Transaction receipt, block timestamp and token transfers, read by us from a public Ethereum RPC node&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Transient storage is a useful tool, but it changes what "temporary" means. A flag that lasts for a transaction is not a flag that lasts for a call, and in a contract that can be called many times per transaction, that difference is the whole security model. Pair that with a privileged contract that will call anything it is told to, and one honest message becomes a key to every escrow.&lt;/p&gt;

&lt;p&gt;Building a bridge, rollup or cross-chain messaging layer? &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;Contact Panda Security&lt;/a&gt; for a review of your message authentication and access-control paths, or see our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>ethereum</category>
      <category>solidity</category>
    </item>
    <item>
      <title>TranchePool Exploit: A Midnight Rollover Drained $1.01M</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:06:45 +0000</pubDate>
      <link>https://dev.to/pandasecurity/tranchepool-exploit-a-midnight-rollover-drained-101m-55c0</link>
      <guid>https://dev.to/pandasecurity/tranchepool-exploit-a-midnight-rollover-drained-101m-55c0</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/tranchepool-utc-rollover-settlement-exploit/" rel="noopener noreferrer"&gt;TranchePool Exploit: A Midnight Rollover Drained $1.01M&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Shortly after midnight UTC on &lt;strong&gt;28 October 2026&lt;/strong&gt;, an attacker drained &lt;strong&gt;1,013,353.61 USDC&lt;/strong&gt; from a &lt;strong&gt;TranchePool on Avalanche C-Chain&lt;/strong&gt; that SlowMist reports is associated with &lt;strong&gt;Valinor Digital, Oatfi and Fence Finance&lt;/strong&gt;. The pool lost its entire USDC balance. There was no oracle manipulation and no reentrancy. The bug was a piece of credit accounting: when the calendar turned to a new UTC day, the pool treated a scheduled principal repayment as if it had actually been paid, even though no USDC arrived. That single bookkeeping step gave the attacker a fresh line of draw capacity, and the attacker used it three seconds after midnight.&lt;/p&gt;

&lt;p&gt;This post walks through what the chain shows, why the root cause is so easy to miss in a review, and what teams building tokenized credit or vault products should test for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened
&lt;/h2&gt;

&lt;p&gt;The attack used two transactions from the same externally owned account, &lt;code&gt;0x6ff1c3ff...2b1246&lt;/code&gt;, both calling a helper contract at &lt;code&gt;0xf09dcf4e...a57805&lt;/code&gt; that the attacker had deployed about fifteen minutes earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transaction 1: the setup (27 October, 23:56:41 UTC).&lt;/strong&gt; The attacker's contract took a flash loan of &lt;strong&gt;1,013,353.611686 USDC&lt;/strong&gt; through Aave v3. That figure is exactly the pool's USDC balance. We read the receipt from a public Avalanche RPC node: the full amount went from the attacker contract into the TranchePool at &lt;code&gt;0xe25cb545...47621a&lt;/code&gt;, the same amount came straight back out of the pool, and &lt;strong&gt;1,013,860.288492 USDC&lt;/strong&gt; was returned to Aave, a premium of roughly 506.68 USDC. After this transaction, the pool still held all of its USDC. Nothing had been stolen yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transaction 2: the drain (28 October, 00:00:03 UTC).&lt;/strong&gt; Three seconds into the new UTC day, the attacker's contract called the pool's draw path again. This time the pool sent &lt;strong&gt;1,013,353.611686 USDC&lt;/strong&gt; of its own funds to the attacker contract, which forwarded it to the attacker's wallet. The pool's balance went to zero.&lt;/p&gt;

&lt;p&gt;Within minutes the attacker moved the funds off Avalanche. Two calls to Circle's CCTP &lt;code&gt;depositForBurnWithHook&lt;/code&gt; at 00:03:41 and 00:05:22 UTC burned about &lt;strong&gt;250,000 USDC&lt;/strong&gt; and &lt;strong&gt;763,375 USDC&lt;/strong&gt; with destination domain 5, which is Solana in CCTP's domain numbering.&lt;/p&gt;

&lt;p&gt;Blockaid was among the first to flag the incident and initially described it as a flash loan used to mint inflated tranche shares. SlowMist's follow-up analysis pointed to the settlement logic instead, and an independent fork replay published by Sentinelleai supports that reading. CertiK Alert summarised it the same way: a scheduled accounting update reopened draw capacity that had already been used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause: A Due Date Was Booked as a Payment
&lt;/h2&gt;

&lt;p&gt;According to SlowMist, the vulnerable pool tracks two numbers for a tranche: &lt;strong&gt;funded principal&lt;/strong&gt; and &lt;strong&gt;outstanding principal&lt;/strong&gt;. The draw function (selector &lt;code&gt;0x8e27c105&lt;/code&gt;) lets a borrower take out up to the difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;available = fundedPrincipal - outstandingPrincipal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That check is reasonable on its own. Outstanding principal goes up when money is drawn and should only go down when money is repaid.&lt;/p&gt;

&lt;p&gt;The flaw was in the settlement step that runs at the &lt;strong&gt;UTC day rollover&lt;/strong&gt;. When a new day began, the pool processed the scheduled principal payment for that date and &lt;strong&gt;subtracted it from outstanding principal&lt;/strong&gt; without confirming that any USDC had actually been received. On paper the loan had been paid down. On-chain, nothing had moved.&lt;/p&gt;

&lt;p&gt;Put the two pieces together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before midnight, the attacker used flash-loaned USDC to set up a position with the pool, drew the full amount back and repaid Aave. Per Sentinelleai's decode of the calldata, one of the setup calls carried the amount of the pool's balance and a timestamp of &lt;code&gt;0x6ae13b00&lt;/code&gt;, which is 00:00:00 UTC on 28 October. The attacker scheduled a repayment for the very next second.&lt;/li&gt;
&lt;li&gt;At midnight, the rollover booked that scheduled payment as received. Outstanding principal dropped by about 1.013M USDC.&lt;/li&gt;
&lt;li&gt;The draw check now saw roughly 1.013M USDC of capacity again, so the same draw, with the same calldata, succeeded a second time and paid out real funds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The flash loan only made the setup capital-free. The theft itself was the rollover.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Clock Was the Only Variable
&lt;/h3&gt;

&lt;p&gt;Sentinelleai forked Avalanche just before midnight and replayed the second transaction's exact calldata with different block timestamps. At 23:59:59 UTC the draw reverted. At 00:00:00 UTC it succeeded and emptied the pool. With the same state and the same input, the only thing that changed the outcome was the block time. That is the signature of a time-dependent accounting bug, not a share-pricing bug.&lt;/p&gt;

&lt;p&gt;One caveat matters here: the pool's source code is &lt;strong&gt;not verified&lt;/strong&gt; on the public explorers. The analyses above, including ours, rest on calldata, events, token transfers, balances and fork replays rather than on reading the original Solidity.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Simplified Model of the Bug
&lt;/h3&gt;

&lt;p&gt;The code below is &lt;strong&gt;simplified and illustrative&lt;/strong&gt;. It is not the pool's real source, which is unverified. It shows the shape of the mistake.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SIMPLIFIED, ILLUSTRATIVE ONLY - not the victim contract's code
contract TranchePoolSketch {
    IERC20 public immutable usdc;
    uint256 public fundedPrincipal;
    uint256 public outstandingPrincipal;

    struct Scheduled { uint64 dueAt; uint256 amount; bool settled; }
    Scheduled[] public schedule;

    function draw(uint256 amount) external {
        _rollover();
        require(amount &amp;lt;= fundedPrincipal - outstandingPrincipal, "no capacity");
        outstandingPrincipal += amount;
        usdc.transfer(msg.sender, amount);
    }

    // BUG: runs when a new UTC day starts and treats "due" as "paid".
    function _rollover() internal {
        for (uint256 i; i &amp;lt; schedule.length; i++) {
            Scheduled storage s = schedule[i];
            if (!s.settled &amp;amp;&amp;amp; block.timestamp &amp;gt;= s.dueAt) {
                s.settled = true;
                outstandingPrincipal -= s.amount; // no USDC was received
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safer design separates "an amount is due" from "an amount has arrived." Outstanding principal should only decrease inside a repayment function that pulls the tokens and measures the balance change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SIMPLIFIED, ILLUSTRATIVE ONLY
function repay(uint256 amount) external {
    uint256 before = usdc.balanceOf(address(this));
    usdc.transferFrom(msg.sender, address(this), amount);
    uint256 received = usdc.balanceOf(address(this)) - before;
    outstandingPrincipal -= received;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scheduled payments that come due without funds should move the position into a "late" or "default" state, which reduces capacity rather than restoring it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Reviews Miss This Class of Bug
&lt;/h2&gt;

&lt;p&gt;Real-world credit brings real-world conventions on-chain: due dates, day counts, payment waterfalls, senior and junior tranches. Each convention adds a place where &lt;strong&gt;state changes without money moving&lt;/strong&gt;. In traditional finance, a back office reconciles the ledger against the bank account. On-chain, the contract is both the ledger and the account, so any line that updates the ledger without checking the token balance becomes an attack surface.&lt;/p&gt;

&lt;p&gt;Three things made this one easy to overlook:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It only triggers at a boundary.&lt;/strong&gt; Tests that run at a fixed timestamp, or that never cross midnight UTC, will never reach the buggy branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The attacker controlled the schedule.&lt;/strong&gt; A due date one second in the future is legal input in most credit systems. Few test suites ask what happens when the borrower picks the due date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events looked normal.&lt;/strong&gt; Monitoring that watches for unusual mints or price moves would see a routine draw. The dangerous transition was a scheduled state update, which produces no obvious alert. That is also why the first public explanation was a share-inflation theory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We saw a related theme in the &lt;a href="https://www.panda-security.xyz/blog/x100-senate-governance-flash-loan-exploit/" rel="noopener noreferrer"&gt;X100 governance flash loan exploit&lt;/a&gt;, where a flash loan made an attack free rather than being the vulnerability itself. And just a day earlier, the &lt;a href="https://www.panda-security.xyz/blog/avalanche-safe-stale-approval-createcall-exploit/" rel="noopener noreferrer"&gt;Avalanche Safe stale approval incident&lt;/a&gt; showed how an assumption made at one moment can be cashed in later by whoever arrives first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Teams and Auditors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Write down the money invariant and enforce it.&lt;/strong&gt; For any lending or credit pool the core rule is short: &lt;em&gt;draw capacity can never exceed assets the contract has actually received.&lt;/em&gt; Encode it as an assertion or an invariant test, not just a comment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Never reduce debt on a schedule.&lt;/strong&gt; Outstanding principal should decrease only when tokens arrive, measured by balance deltas. A payment that comes due without funds is a missed payment, and it should tighten the position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Fuzz across time boundaries.&lt;/strong&gt; Property tests should warp across the UTC day rollover, test due dates equal to &lt;code&gt;block.timestamp&lt;/code&gt;, test due dates set to the next second, and test due dates supplied by the caller. Foundry's &lt;code&gt;vm.warp&lt;/code&gt; makes this cheap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Treat user-chosen dates as adversarial input.&lt;/strong&gt; If a borrower can set the schedule, assume they will pick the value that makes your accounting worst. Enforce minimum intervals or require repayments to be prefunded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Make draws idempotent.&lt;/strong&gt; Replaying identical calldata should not succeed twice unless the state genuinely justifies it. Per-draw nonces or explicit drawdown records make double draws visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Verify your source code.&lt;/strong&gt; An unverified contract slows down every responder during an incident and hides the logic from independent reviewers before one. Our &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt; covers this alongside the testing items above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovery Status
&lt;/h2&gt;

&lt;p&gt;At 03:16:23 UTC on 28 October, an address sent the attacker's wallet an on-chain message on behalf of Fence. The message offers a &lt;strong&gt;10% recovery bounty&lt;/strong&gt; in exchange for returning 90% of the verified affected funds and lists &lt;code&gt;recovery@fence.finance&lt;/code&gt; as a contact. As of this writing we have not seen a confirmed return of funds or a full statement on depositor losses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Addresses and Transactions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Vulnerable TranchePool: &lt;a href="https://snowtrace.io/address/0xe25cb545bdd47a8ec2d08001cb5661b00d47621a" rel="noopener noreferrer"&gt;&lt;code&gt;0xe25cb545bdd47a8ec2d08001cb5661b00d47621a&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Attacker wallet: &lt;a href="https://snowtrace.io/address/0x6ff1c3ffa622e1062168f3497a275be9cc2b1246" rel="noopener noreferrer"&gt;&lt;code&gt;0x6ff1c3ffa622e1062168f3497a275be9cc2b1246&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Attacker contract: &lt;a href="https://snowtrace.io/address/0xf09dcf4ede2a88bd316ff355dfc333a281a57805" rel="noopener noreferrer"&gt;&lt;code&gt;0xf09dcf4ede2a88bd316ff355dfc333a281a57805&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Setup transaction (flash loan): &lt;a href="https://snowtrace.io/tx/0x19ab145e7f771549ca2f1da4b1768d7f728834015b2480ddb5be19c9769899fd" rel="noopener noreferrer"&gt;&lt;code&gt;0x19ab145e...9769899fd&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Drain transaction: &lt;a href="https://snowtrace.io/tx/0x3f6b23292a2e9c2956c7ac56d0b1f79b0271b49edec2156d2675ddf1de058c77" rel="noopener noreferrer"&gt;&lt;code&gt;0x3f6b2329...1de058c77&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CCTP burn, ~250,000 USDC: &lt;a href="https://snowtrace.io/tx/0x0e974743161ddb166d5ad698b9a2b0c9ca2ddf5f569a532e6c5665ce2c92218b" rel="noopener noreferrer"&gt;&lt;code&gt;0x0e974743...92c2218b&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CCTP burn, ~763,375 USDC: &lt;a href="https://snowtrace.io/tx/0x51bb85e171585a2ec7ec7bd918cd6a08e916e38f79cf2a17b5e5211ee3b9f397" rel="noopener noreferrer"&gt;&lt;code&gt;0x51bb85e1...ee3b9f397&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bounty message: &lt;a href="https://snowtrace.io/tx/0xaf19a8a668a8d4b759197ed33607db84cde73a801068e741d1a887e5c02f4894" rel="noopener noreferrer"&gt;&lt;code&gt;0xaf19a8a6...b2f4894&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://x.com/SlowMist_Team/status/2115281126516490514" rel="noopener noreferrer"&gt;SlowMist TI Alert on X&lt;/a&gt;: root cause, draw selector, attacker and vulnerable contract addresses, transaction links&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/blockaid_/status/2115249066787340562" rel="noopener noreferrer"&gt;Blockaid alert on X&lt;/a&gt;: first detection and the initial share-inflation reading&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/CertiKAlert/status/2115289650025923015" rel="noopener noreferrer"&gt;CertiK Alert on X&lt;/a&gt;: independent summary of the reopened draw capacity&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/sentinelleai/one-second-of-block-time-replaying-the-fence-finance-tranchepool-drain-hni"&gt;Sentinelleai fork replay&lt;/a&gt;: calldata decode, timestamp analysis and fork replay&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hacked.slowmist.io/" rel="noopener noreferrer"&gt;SlowMist Hacked database&lt;/a&gt;: incident entry for 28 October 2026&lt;/li&gt;
&lt;li&gt;Transaction receipts, token transfers, CCTP calls and the bounty message, read by us from a public Avalanche C-Chain RPC node&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The pool did exactly what its ledger told it to do. The ledger was wrong for one second, at a boundary nobody tested, on a date the attacker chose. When on-chain credit borrows TradFi conventions, every scheduled state change needs the same question: did the money actually arrive?&lt;/p&gt;

&lt;p&gt;Building a lending, RWA or tranche-based protocol? &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;Contact Panda Security&lt;/a&gt; for a review of your accounting invariants and time-dependent logic, or see our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>solidity</category>
    </item>
    <item>
      <title>Avalanche Safe: A Stale Approval to an Empty Address</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Tue, 27 Oct 2026 23:06:39 +0000</pubDate>
      <link>https://dev.to/pandasecurity/avalanche-safe-a-stale-approval-to-an-empty-address-3kda</link>
      <guid>https://dev.to/pandasecurity/avalanche-safe-a-stale-approval-to-an-empty-address-3kda</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/avalanche-safe-stale-approval-createcall-exploit/" rel="noopener noreferrer"&gt;Avalanche Safe: A Stale Approval to an Empty Address&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;26 October 2026&lt;/strong&gt;, a &lt;strong&gt;2-of-7 Safe multisig on Avalanche C-Chain&lt;/strong&gt; holding roughly &lt;strong&gt;$7.45M&lt;/strong&gt; of trading inventory was emptied in a single block. No signer key was stolen, no signer was phished, and the Safe contracts did exactly what they were built to do. The money left through four token approvals the Safe itself had signed seven months earlier, pointing at an address that, on Avalanche, had no code at all.&lt;/p&gt;

&lt;p&gt;The original attacker never got the funds. An MEV bot copied the attack and landed first in the same block, then returned most of the assets the next day in exchange for a bounty. The outcome was lucky. The bug class is not, and it is one every team that deploys to several EVM chains should understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened
&lt;/h2&gt;

&lt;p&gt;According to the security monitor Defimon, which first reported the incident, and an on-chain reconstruction published by BlockintAI, the sequence was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;7 March 2026, 16:10 UTC.&lt;/strong&gt; The Safe executed a batch transaction that granted &lt;strong&gt;unlimited&lt;/strong&gt; allowances for &lt;strong&gt;WAVAX, USDC, WETH.e and BTC.b&lt;/strong&gt; to &lt;code&gt;0x3725bd4d175283108156c3f15f86e1c51266155d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On Ethereum, Base and Arbitrum, that address hosts the operator's own pool contract. On Avalanche, the address was empty. The approvals were never revoked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;26 October 2026, 12:56 UTC, block 98,605,391.&lt;/strong&gt; A fresh wallet, &lt;code&gt;0x4c48b79b97516719444056d2bf0dc9564138fa0a&lt;/code&gt;, funded about 27 minutes earlier with 6.77 AVAX, sent one Multicall3 transaction designed to claim the empty address and sweep the Safe.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;same block&lt;/strong&gt;, at position 0, MEV bot &lt;code&gt;0x36b112875f921e51f5ea8ff39b374960694d21b3&lt;/code&gt; ran a copy of that transaction. It succeeded. The attacker's transaction, at position 2, reverted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We confirmed both receipts against a public Avalanche RPC node: the bot's transaction has status success at index 0 in block 98,605,391, and the attacker's transaction has status failed at index 2 in the same block.&lt;/p&gt;

&lt;p&gt;Per BlockintAI's reading of the logs, the bot's transaction moved these balances out of the Safe:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;USDC&lt;/td&gt;
&lt;td&gt;3,090,123.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WAVAX&lt;/td&gt;
&lt;td&gt;350,375.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WETH.e&lt;/td&gt;
&lt;td&gt;181.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BTC.b&lt;/td&gt;
&lt;td&gt;10.79&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Root Cause: An Approval Is a Promise to an Address
&lt;/h2&gt;

&lt;p&gt;ERC-20 &lt;code&gt;approve&lt;/code&gt; does not know or care whether the spender is a contract, which contract it is, or whether anything exists there yet. It writes a number into a mapping keyed by an address. Whoever later controls code at that address can call &lt;code&gt;transferFrom&lt;/code&gt; up to the allowance.&lt;/p&gt;

&lt;p&gt;On most days an approval to an empty address is inert, because nothing can call from it. It turns into a live liability the moment someone can deploy code to that exact address. Here, someone could.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the Address Was Claimable
&lt;/h3&gt;

&lt;p&gt;Contracts created with the &lt;code&gt;CREATE&lt;/code&gt; opcode get an address derived only from the deployer's address and the deployer's nonce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;keccak256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rlp&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;deployer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;]))[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the property teams rely on to get "the same address on every chain": use the same deployer, make the same number of deployments, and the results line up. It is also the weakness. If the deployer exists on a new chain but its nonce has not reached the right value, anyone who can push transactions through that deployer can reach it first.&lt;/p&gt;

&lt;p&gt;Both Defimon and BlockintAI identify the deployer behind &lt;code&gt;0x3725bd…155d&lt;/code&gt; on the other chains as Safe's public &lt;strong&gt;CreateCall&lt;/strong&gt; helper (&lt;code&gt;0xb22d635d552ec95142e2abe3ffb859ea7d7c0316&lt;/code&gt;), a contract that deploys whatever bytecode the caller hands it. CreateCall itself is installed at a deterministic address through the Safe singleton factory, so anyone can put it on a chain where it does not yet exist. BlockintAI computed that CreateCall's &lt;strong&gt;13th&lt;/strong&gt; deployment lands at &lt;code&gt;0x3725bd…155d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before 26 October, CreateCall had never been deployed on Avalanche. So the path to the Safe's allowances was open to anyone who would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy CreateCall on Avalanche through the singleton factory.&lt;/li&gt;
&lt;li&gt;Make &lt;strong&gt;12 throwaway deployments&lt;/strong&gt; through it to advance its nonce.&lt;/li&gt;
&lt;li&gt;Deploy a &lt;strong&gt;sweeper&lt;/strong&gt; contract as the 13th, landing at &lt;code&gt;0x3725bd…155d&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Have the sweeper call &lt;code&gt;transferFrom&lt;/code&gt; on all four tokens, pulling the Safe's full balances.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of that fits in one transaction, which is exactly what the attacker's Multicall3 batch tried to do. The allowance had been waiting for a caller for about 233 days.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Simplified Model of the Sweep
&lt;/h3&gt;

&lt;p&gt;The code below is a &lt;strong&gt;simplified illustration&lt;/strong&gt; of the mechanism, not the attacker's or the bot's actual bytecode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
// Simplified illustration only.
pragma solidity ^0.8.20;

interface IERC20 {
    function balanceOf(address) external view returns (uint256);
    function allowance(address, address) external view returns (uint256);
    function transferFrom(address, address, uint256) external returns (bool);
}

interface ICreateCall {
    function performCreate(uint256 value, bytes memory code) external returns (address);
}

contract Sweeper {
    // Deployed as CreateCall's 13th contract, so address(this) equals
    // the address the victim Safe approved months earlier.
    constructor(address victim, address receiver, IERC20[] memory tokens) {
        for (uint256 i; i &amp;lt; tokens.length; i++) {
            uint256 bal = tokens[i].balanceOf(victim);
            uint256 allowed = tokens[i].allowance(victim, address(this));
            uint256 amt = bal &amp;lt; allowed ? bal : allowed;
            if (amt &amp;gt; 0) tokens[i].transferFrom(victim, receiver, amt);
        }
    }
}

contract Claimer {
    function run(ICreateCall createCall, bytes calldata dummy, bytes calldata sweeper) external {
        // Burn nonces 1..12 with throwaway contracts.
        for (uint256 i; i &amp;lt; 12; i++) createCall.performCreate(0, dummy);
        // Nonce 13 lands on the approved address.
        createCall.performCreate(0, sweeper);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in this flow requires a signature from the Safe. The Safe's earlier signatures are already on-chain, in the form of allowances.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Front-Run and the Bounty
&lt;/h2&gt;

&lt;p&gt;Because the attacker's transaction was fully self-contained and profitable, a bot that simulates pending transactions and replays profitable ones with its own address could copy it. That is what happened. BlockintAI reports the bot later moved the funds to a holding wallet, &lt;code&gt;0x11eb9b677510e2594857f4d137fd12a7fef39bc5&lt;/code&gt;, and posted an on-chain message pointing to the original attack and promising to return the assets to &lt;code&gt;0x35c6aa248e9495E0572598fBfE0528aCbAC2807C&lt;/code&gt;, asking for a 5 to 10% bounty.&lt;/p&gt;

&lt;p&gt;On 27 October, one of the Safe's owner addresses agreed on-chain to let the bot keep &lt;strong&gt;42,500 WAVAX&lt;/strong&gt; (about &lt;strong&gt;$371K&lt;/strong&gt;, per Defimon). BlockintAI then observed the return of all of the USDC, WETH.e and BTC.b, plus 307,834.86 WAVAX, with the 42,500 WAVAX bounty remaining in the holding wallet. CryptoTimes, writing earlier the same day, described the negotiation as still in progress, so treat the final settlement figures as BlockintAI's reading of the chain at the time of its report.&lt;/p&gt;

&lt;p&gt;Two side notes from the reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The operator moved funds on other chains.&lt;/strong&gt; Defimon reports that from 13:48 UTC on 26 October, the same operator's signers consolidated assets from Safes on HyperEVM, Base, Robinhood Chain, BNB Chain and Ethereum into &lt;code&gt;0x70d54baf9ffa48b8fe4e843774a44e24ff452099&lt;/code&gt;, which it puts at about $38.7M. That is a sensible move: if one chain had an orphaned approval, others might too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Address poisoning followed immediately.&lt;/strong&gt; BlockintAI saw fake "USDC" and "BTC.b" tokens emitting transfers toward a lookalike of the return address. Anyone copying addresses from wallet history during an incident is a target.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bot giving the money back was a choice, not a control. If the attacker had used a private transaction route, or the bot had decided to keep the funds, the loss would have been the full $7.45M.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Pattern Keeps Coming Back
&lt;/h2&gt;

&lt;p&gt;We have written before about how cross-chain assumptions break, for example in the &lt;a href="https://www.panda-security.xyz/blog/wax-bridge-negative-fee-exploit/" rel="noopener noreferrer"&gt;WAX bridge negative fee exploit&lt;/a&gt;, and how wallet-level logic can be replayed in ways its authors never expected in the &lt;a href="https://www.panda-security.xyz/blog/argent-style-wallet-recovery-replay-exploit/" rel="noopener noreferrer"&gt;Argent-style wallet recovery replay&lt;/a&gt;. This incident is a cleaner version of the same lesson: &lt;strong&gt;an assumption that holds on one chain was carried to another chain where it did not hold.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same shape shows up in several places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-approving "future" deployments&lt;/strong&gt; on a chain where the contract has not been deployed yet, so the deployment script can run later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy-pasting multisig transaction batches&lt;/strong&gt; between chains and only changing the chain ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counterfactual addresses&lt;/strong&gt; derived from &lt;code&gt;CREATE&lt;/code&gt; with a factory anyone can call, rather than &lt;code&gt;CREATE2&lt;/code&gt; with a salt and init code hash that only the intended bytecode can reach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treasury allowances that outlive the integration&lt;/strong&gt; they were made for.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons for Teams and Auditors
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Verify spender code before every approval, on that chain.&lt;/strong&gt; Before a Safe signs an &lt;code&gt;approve&lt;/code&gt;, check that the spender has code on the target chain and that its code hash matches what you expect. A Safe transaction guard or a pre-sign checklist can enforce "no approvals to addresses without code."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Do not rely on &lt;code&gt;CREATE&lt;/code&gt; nonce alignment across chains.&lt;/strong&gt; If you need the same address everywhere, deploy with &lt;code&gt;CREATE2&lt;/code&gt; (or &lt;code&gt;CREATE3&lt;/code&gt;) from a factory where the salt is bound to your deployer, so nobody else can land a different contract at your address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deploy first, approve second.&lt;/strong&gt; Approvals should be the last step of a rollout on a chain, after the contract is live and verified, never part of a batch that anticipates a later deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Make allowances exact and short-lived.&lt;/strong&gt; Unlimited allowances from treasuries and trading Safes turn every mistake into a full-balance loss. Approve the amount needed, or use flows like Permit2 with expiries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Audit allowances periodically, across every chain.&lt;/strong&gt; An allowance inventory per Safe, per chain, with an owner and an expiry, would have flagged this one months ago. Our &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt; covers operational controls like this alongside code review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Treat multisig operations as code.&lt;/strong&gt; A batch transaction is a deployment. It deserves review, simulation on the target chain and a diff against what was intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Addresses and Transactions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Victim Safe (Avalanche): &lt;code&gt;0x631fca…b9b8&lt;/code&gt; (shortened as in BlockintAI's report)&lt;/li&gt;
&lt;li&gt;Approved address: &lt;a href="https://snowtrace.io/address/0x3725bd4d175283108156c3f15f86e1c51266155d" rel="noopener noreferrer"&gt;&lt;code&gt;0x3725bd4d175283108156c3f15f86e1c51266155d&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Attacker wallet: &lt;a href="https://snowtrace.io/address/0x4c48b79b97516719444056d2bf0dc9564138fa0a" rel="noopener noreferrer"&gt;&lt;code&gt;0x4c48b79b97516719444056d2bf0dc9564138fa0a&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MEV bot: &lt;a href="https://snowtrace.io/address/0x36b112875f921e51f5ea8ff39b374960694d21b3" rel="noopener noreferrer"&gt;&lt;code&gt;0x36b112875f921e51f5ea8ff39b374960694d21b3&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bot holding wallet: &lt;a href="https://snowtrace.io/address/0x11eb9b677510e2594857f4d137fd12a7fef39bc5" rel="noopener noreferrer"&gt;&lt;code&gt;0x11eb9b677510e2594857f4d137fd12a7fef39bc5&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CreateCall: &lt;a href="https://etherscan.io/address/0xb22d635d552ec95142e2abe3ffb859ea7d7c0316#code" rel="noopener noreferrer"&gt;&lt;code&gt;0xb22d635d552ec95142e2abe3ffb859ea7d7c0316&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;March 7 approvals: &lt;a href="https://snowtrace.io/tx/0x6952f80bbbcad4368b020bcda600965cf0a520a8694db431b6b1521842d3829e" rel="noopener noreferrer"&gt;&lt;code&gt;0x6952f80b...42d3829e&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Failed attacker transaction: &lt;a href="https://snowtrace.io/tx/0xf1f87d370290afe269ae8eb30fe8e5e4694ad153914a2de6d980641f4a331289" rel="noopener noreferrer"&gt;&lt;code&gt;0xf1f87d37...4a331289&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MEV bot transaction: &lt;a href="https://snowtrace.io/tx/0xf88a7960a07832e1bf5be64edbd12a9e615b4f63959a4b87a147f0c63c007ed5" rel="noopener noreferrer"&gt;&lt;code&gt;0xf88a7960...3c007ed5&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bounty agreement message: &lt;a href="https://snowtrace.io/tx/0xd2916fecdf3f0749692830918bd0ac8059c4d87e62d174181e609a7076911afc" rel="noopener noreferrer"&gt;&lt;code&gt;0xd2916fec...76911afc&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://x.com/DefimonAlerts/status/2115005055619260758" rel="noopener noreferrer"&gt;Defimon Alerts on X&lt;/a&gt;: first report, the approved address, the 2-of-7 setup and the $7.45M figure&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://x.com/DefimonAlerts/status/2114940540730970304" rel="noopener noreferrer"&gt;Defimon Alerts on X, bot's on-chain message&lt;/a&gt;: the bot's offer to return funds&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blockint.ai/blog/avalanche-safe-stale-approval-createcall" rel="noopener noreferrer"&gt;BlockintAI research post&lt;/a&gt;: on-chain reconstruction, token amounts, CreateCall nonce analysis and return transfers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.cryptotimes.io/2026/10/27/avalanche-safe-loses-7-45m-to-misconfigured-approvals-mev-bot-front-runs-exploit/" rel="noopener noreferrer"&gt;CryptoTimes report&lt;/a&gt;: independent summary of the approvals, the front-run and the bounty negotiation&lt;/li&gt;
&lt;li&gt;Transaction receipts for the attacker and bot transactions, read by us from a public Avalanche C-Chain RPC node&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This Safe was not hacked in the usual sense. It kept a promise it had made months earlier to an address it never checked. On multichain deployments, "same address" is an assumption you have to prove on every chain, every time you sign.&lt;/p&gt;

&lt;p&gt;Running treasuries or contracts across several EVM chains? &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;Contact Panda Security&lt;/a&gt; for a review of your deployment scripts, multisig operations and allowance hygiene, or see our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>solidity</category>
    </item>
    <item>
      <title>Initia Exploit: A MoveVM Cache Bug Emptied Its Bridges</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Mon, 26 Oct 2026 23:12:53 +0000</pubDate>
      <link>https://dev.to/pandasecurity/initia-exploit-a-movevm-cache-bug-emptied-its-bridges-2ij1</link>
      <guid>https://dev.to/pandasecurity/initia-exploit-a-movevm-cache-bug-emptied-its-bridges-2ij1</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/initia-movevm-script-cache-bridge-exploit/" rel="noopener noreferrer"&gt;panda-security.xyz&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;23 October 2026 at 14:59 UTC&lt;/strong&gt;, an attacker used a flaw in the script cache of Initia's MoveVM to credit itself with &lt;strong&gt;unbacked WETH, iUSD and USDC&lt;/strong&gt; on the Initia L1. Within about seven minutes, real assets started leaving Ethereum-side bridge reserves. By the time validators halted the L1 and every rollup at &lt;strong&gt;15:41:30 UTC&lt;/strong&gt;, roughly &lt;strong&gt;$2.7M&lt;/strong&gt; had been bridged out and swapped into &lt;strong&gt;1,135.52 ETH&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No token contract on Ethereum was hacked, and no bridge signature was forged. The bridges did exactly what they were designed to do: they burned a token on Initia and released the matching asset on Ethereum. The problem is that the token on Initia was never backed. This post walks through what is confirmed, what is still unknown, and what bridge and L1 teams should take from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Victim:&lt;/strong&gt; Initia, a Cosmos SDK L1 that runs MoveVM and hosts a network of rollups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause (per Initia):&lt;/strong&gt; a bug in MoveVM's script caching let malicious code "bypass security checks" and spoof VM state, crediting balances without changing the recorded total supply. Initia has not yet published the technical post-mortem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cash-out:&lt;/strong&gt; about &lt;strong&gt;105 WETH&lt;/strong&gt; and &lt;strong&gt;~1.4M AUSD0&lt;/strong&gt; over LayerZero, plus &lt;strong&gt;~1.08M USDC&lt;/strong&gt; through Noble and Circle's CCTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proceeds:&lt;/strong&gt; swapped to ETH and parked in &lt;code&gt;0x358ef013...41cac8&lt;/code&gt;, which still held &lt;strong&gt;1,135.52 ETH&lt;/strong&gt; and had never sent a transaction when we checked it on 27 October.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who pays:&lt;/strong&gt; the reserves behind bridged assets. Bitquery measured Noble's USDC reserve for Initia falling from &lt;strong&gt;$1.09M to $63&lt;/strong&gt;, the ETH adapter losing about &lt;strong&gt;96%&lt;/strong&gt; and the AUSD adapter about &lt;strong&gt;71%&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native INIT:&lt;/strong&gt; not minted, and Initia says its supply is unaffected.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Initia Is and Why Bridged Tokens Matter
&lt;/h2&gt;

&lt;p&gt;Initia is a layer 1 built on the Cosmos SDK, with MoveVM as its smart contract engine and a family of rollups that settle to it. Most of the dollar and ether liquidity on the chain is not native. It arrives through bridges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ETH and AUSD&lt;/strong&gt; come from Ethereum through LayerZero OFT adapters. An adapter locks the real asset on Ethereum, and an omnichain token is credited on Initia. AUSD's Initia-side copy is called AUSD0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;USDC&lt;/strong&gt; comes from Noble, the Cosmos chain that issues Circle's USDC, and leaves for Ethereum through CCTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iUSD&lt;/strong&gt; is Initia's own dollar token, issued one-for-one against AUSD0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The security of every one of these copies reduces to a single invariant: &lt;strong&gt;the balances on Initia must never exceed what is locked behind them.&lt;/strong&gt; If the destination chain can be convinced that someone owns tokens they never deposited, the reserve on the home chain will pay the difference to whoever burns them first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Initia Has Confirmed
&lt;/h2&gt;

&lt;p&gt;Initia's official statement on 24 October says an attacker "exploited a bug in Initia MoveVM's script caching that allowed malicious code to bypass security checks." It adds that the attacker "artificially spoofed the MoveVM state," receiving unbacked WETH, iUSD and USDC "without changing the corresponding token's total supply." Co-founder Zon described it as a day-zero bug in Initia's MoveVM on the Cosmos SDK.&lt;/p&gt;

&lt;p&gt;That last detail matters for monitoring. The forged balances did &lt;strong&gt;not&lt;/strong&gt; appear in each token's recorded supply. A dashboard that watched only &lt;code&gt;total_supply&lt;/code&gt; on Initia would have shown nothing unusual while millions of dollars in balances appeared.&lt;/p&gt;

&lt;p&gt;Initia has not said which cache, which check, or which code path was involved. A fix and audit were promised "in a few days," and the L1 and rollups were still halted at the time of writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Chain Shows
&lt;/h2&gt;

&lt;p&gt;Two independent on-chain write-ups, from &lt;strong&gt;D2 Finance&lt;/strong&gt; (relayed by 0xCERT) and from &lt;strong&gt;Bitquery&lt;/strong&gt;, line up on the shape of the attack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preparation
&lt;/h3&gt;

&lt;p&gt;Bitquery reports that the attacker's Ethereum wallet, &lt;code&gt;0xdd922c84...f6edfa&lt;/code&gt;, received &lt;strong&gt;0.098 ETH from Tornado Cash&lt;/strong&gt; the same morning, sent small test transfers toward Osmosis, and bridged a little ETH to Initia. It then received &lt;strong&gt;141 INIT&lt;/strong&gt; for gas from an Osmosis wallet and uploaded its own program. The first attempt failed. The second worked.&lt;/p&gt;

&lt;h3&gt;
  
  
  The forged balances
&lt;/h3&gt;

&lt;p&gt;D2 Finance pointed to a single mint transaction at &lt;strong&gt;14:59:26 UTC&lt;/strong&gt; that called one Move function to credit &lt;strong&gt;2,000,000 AUSD0&lt;/strong&gt;, with the same pattern for USDC and ETH. Bitquery counted four runs of the attacker's program within &lt;strong&gt;13 seconds&lt;/strong&gt;, producing about &lt;strong&gt;$7.48M&lt;/strong&gt; of iUSD, USDC, AUSD0 and WETH at face value. Both agree that nothing was locked anywhere to back it.&lt;/p&gt;

&lt;p&gt;Face value overstates the damage. About 3M of that was iUSD, which exists only on Initia and had no bridge to drain. Only the copies of real assets could be redeemed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cash-out
&lt;/h3&gt;

&lt;p&gt;We checked the Ethereum side ourselves against a public RPC node:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In block &lt;strong&gt;26,256,091&lt;/strong&gt;, the AUSD OFT adapter &lt;code&gt;0x38b8b7ca...f200c844&lt;/code&gt; transferred exactly &lt;strong&gt;1,400,000 AUSD&lt;/strong&gt; to the attacker.&lt;/li&gt;
&lt;li&gt;In block &lt;strong&gt;26,256,115&lt;/strong&gt;, the ETH OFT adapter &lt;code&gt;0x66a503a1...0babbc1bde&lt;/code&gt; released exactly &lt;strong&gt;105 ETH&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;1.08M USDC&lt;/strong&gt; was minted to Ethereum via CCTP after leaving Noble's reserve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each batch was swapped to ETH within minutes, mostly through 1inch according to Bitquery. Then, at &lt;strong&gt;15:41:35 UTC&lt;/strong&gt;, five seconds after the halt, the wallet sent &lt;strong&gt;1,135.52 ETH&lt;/strong&gt; to a fresh address, &lt;code&gt;0x358ef013...41cac8&lt;/code&gt;. That address had a transaction count of zero and the same balance when we read it on 27 October.&lt;/p&gt;

&lt;h3&gt;
  
  
  What did not get out
&lt;/h3&gt;

&lt;p&gt;Bitquery found that three USDC withdrawals bounced because Noble's reserve no longer held enough. A final 200,000 AUSD0 message stalled because LayerZero Labs' own verifier did not sign it, and the AUSD adapter blocked it later that day. Roughly &lt;strong&gt;$1.55M&lt;/strong&gt; of copies of real assets, plus the unsellable iUSD, were still on Initia when the chain stopped.&lt;/p&gt;

&lt;p&gt;Ordinary users paid as well. After the attacker's withdrawals, eleven normal USDC redemptions drained most of what remained in Noble's reserve, and Bitquery counted &lt;strong&gt;55&lt;/strong&gt; later withdrawals that failed before the halt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Cache Bug Becomes a Mint
&lt;/h2&gt;

&lt;p&gt;Initia has not published the mechanism, so we will not guess at its code. The general failure class is well known to anyone who audits VMs, and it is worth stating plainly.&lt;/p&gt;

&lt;p&gt;A Move VM does expensive work before it runs a script: it deserializes the bytecode, runs the &lt;strong&gt;bytecode verifier&lt;/strong&gt;, and checks type and resource-safety rules. Those rules are what stop a script from creating a &lt;code&gt;Coin&lt;/code&gt; or &lt;code&gt;FungibleAsset&lt;/code&gt; value out of thin air or writing to storage it does not own. Caching the result is a reasonable optimisation. It becomes dangerous when &lt;strong&gt;the cache key does not fully bind what was verified&lt;/strong&gt;. If the key misses the exact bytes, the dependency set, or the context the check depended on, a later execution can reuse a "verified" result for code that was never verified.&lt;/p&gt;

&lt;p&gt;In a resource-oriented VM, skipping verification is close to the worst case. Resource safety is the mint authority. Bypass it, and a balance can exist without the module that controls supply ever being asked. That is consistent with Initia's description of balances that appeared while recorded supply did not move.&lt;/p&gt;

&lt;p&gt;The lesson for auditors: in VM and runtime reviews, &lt;strong&gt;every cache in front of a security check is itself a security boundary.&lt;/strong&gt; Review what the key contains, when entries are invalidated, and whether a cache hit can ever skip a check that a miss would run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bridge Side: Trust Without Limits
&lt;/h2&gt;

&lt;p&gt;The VM bug created the forged balances. The bridges turned them into $2.7M. A bridge cannot tell a forged balance from a real one if the source chain's state says it is real, so the last line of defense is how much it will release, and how fast.&lt;/p&gt;

&lt;p&gt;Here is a &lt;strong&gt;simplified, illustrative&lt;/strong&gt; Solidity sketch of an outflow limit on a lockbox adapter. It is not LayerZero's or Initia's code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration only. Not production code.
contract RateLimitedLockbox {
    uint256 public constant WINDOW = 1 hours;
    uint256 public maxOutflowPerWindow;   // e.g. 10% of locked reserves
    uint256 public windowStart;
    uint256 public outflowInWindow;

    function _release(address to, uint256 amount) internal {
        if (block.timestamp &amp;gt;= windowStart + WINDOW) {
            windowStart = block.timestamp;
            outflowInWindow = 0;
        }
        outflowInWindow += amount;
        require(outflowInWindow &amp;lt;= maxOutflowPerWindow, "outflow limit");
        token.transfer(to, amount);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a cap of, say, 10% of reserves per hour, a 1.4M AUSD release out of a larger pool, plus a 105 ETH release out of a reserve that ended 96% drained, would have hit the limit and paused. That would have bought the 42 minutes Initia needed to halt the chain.&lt;/p&gt;

&lt;p&gt;It is worth noting what did work. LayerZero's verifier set includes a LayerZero Labs verifier, and the final AUSD message stalled when it did not sign. Noble's reserve could only pay what it held, which capped the USDC loss. Earlier we covered &lt;a href="https://www.panda-security.xyz/blog/wax-bridge-negative-fee-exploit/" rel="noopener noreferrer"&gt;a negative fee that drained the WAX bridge&lt;/a&gt; and &lt;a href="https://www.panda-security.xyz/blog/sui-bridge-stale-withdrawals-cetus-attacker/" rel="noopener noreferrer"&gt;a Sui bridge claim bot that paid the Cetus attacker&lt;/a&gt;. The bugs differ, but each raises the same audit question about a bridge: how much can leave before anyone is able to react?&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Prevent This
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Treat VM caches as part of the trusted computing base
&lt;/h3&gt;

&lt;p&gt;Any cache that stores a verification, type-check or permission result must be keyed by everything the result depended on: the full code hash, dependency versions, and the execution context. Add tests that try to reach execution through a cache hit with code the verifier would reject.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitor balances against supply, not just supply
&lt;/h3&gt;

&lt;p&gt;Initia's forged balances left recorded supply untouched. An invariant check that sums balances for bridged assets, or reconciles each bridged token against its remote lockbox, would have flagged the attack in its first block.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate-limit bridge releases
&lt;/h3&gt;

&lt;p&gt;Per-asset, per-window outflow caps on lockboxes and mint limits on the destination side turn an unlimited drain into a bounded one. Pair them with an automated pause when a cap is hit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Require independent verifiers
&lt;/h3&gt;

&lt;p&gt;The one AUSD message that stalled was the one that lacked a signature from an independent verifier. Bridges should require more than one independent party to approve a release, and those parties should be allowed to refuse when the amount looks wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rehearse the halt
&lt;/h3&gt;

&lt;p&gt;Initia stopped the chain 42 minutes after the first forged balance appeared. That is fast for a full L1 halt, and still long enough to lose $2.7M. Chains with bridge exposure should know exactly who can pause which bridge, and practice doing it.&lt;/p&gt;

&lt;p&gt;For a broader pre-launch list, see our &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Addresses and Transactions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Attacker wallet on Ethereum: &lt;a href="https://etherscan.io/address/0xdd922c841d85e12dac2dc2eef5a5e29cfff6edfa" rel="noopener noreferrer"&gt;&lt;code&gt;0xdd922c841d85e12dac2dc2eef5a5e29cfff6edfa&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Proceeds wallet (1,135.52 ETH): &lt;a href="https://etherscan.io/address/0x358ef0130f7a5aa516dcd81bdc80b18cde41cac8" rel="noopener noreferrer"&gt;&lt;code&gt;0x358ef0130f7a5aa516dcd81bdc80b18cde41cac8&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;1.4M AUSD release: &lt;a href="https://etherscan.io/tx/0x034c1076a08091b3221ef1e7bbcfb47dfeda93a6e460888f2d1bc124be17f3b6" rel="noopener noreferrer"&gt;&lt;code&gt;0x034c1076...be17f3b6&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;105 ETH release: &lt;a href="https://etherscan.io/tx/0x5cd422fd8166abf5d8632e9919bfedf6e26f5b77cf732cc797d863450887d7e9" rel="noopener noreferrer"&gt;&lt;code&gt;0x5cd422fd...0887d7e9&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Transfer to the proceeds wallet: &lt;a href="https://etherscan.io/tx/0x60f15b7e96111f11af54a2a0c9adc2ff33fef6b6a3c1a3b073a55c1421043e06" rel="noopener noreferrer"&gt;&lt;code&gt;0x60f15b7e...1043e06&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AUSD OFT adapter on Ethereum: &lt;a href="https://etherscan.io/address/0x38b8b7ca28e672018a299b20565aca8cf200c844" rel="noopener noreferrer"&gt;&lt;code&gt;0x38b8b7ca28e672018a299b20565aca8cf200c844&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ETH OFT adapter on Ethereum: &lt;a href="https://etherscan.io/address/0x66a503a1060ab3f2b1aaabed613fe30babbc1bde" rel="noopener noreferrer"&gt;&lt;code&gt;0x66a503a1060ab3f2b1aaabed613fe30babbc1bde&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Initia mint transaction (per D2 Finance): &lt;code&gt;67423E84815CEC63632BD4676CA22D33567F3E02C3187281C9B6BFD593CBA7AF&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://x.com/initia/status/2113915002008612945" rel="noopener noreferrer"&gt;Initia's official statement on X&lt;/a&gt;: root cause description, amounts and proceeds address&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.0xcert.com/advisory/0xCERT-2026-0267" rel="noopener noreferrer"&gt;0xCERT advisory 0xCERT-2026-0267&lt;/a&gt;: summary of D2 Finance's on-chain analysis, Agora's clarification and indicators of compromise&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bitquery.io/investigations/initia-hack" rel="noopener noreferrer"&gt;Bitquery investigation&lt;/a&gt;: preparation timeline, $7.48M face value, reserve drawdowns and blocked messages&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://lecoin.news/protocols-policy/2026-10-24-initia-official-exploit-statement/" rel="noopener noreferrer"&gt;Le Coin report&lt;/a&gt;: independent confirmation of the proceeds wallet balance&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.tokenpost.com/news/technology/40153" rel="noopener noreferrer"&gt;TokenPost report&lt;/a&gt;: independent summary of Initia's statement&lt;/li&gt;
&lt;li&gt;Transaction receipts, block timestamps and the proceeds wallet's balance and nonce, read by us from a public Ethereum RPC node&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The Initia exploit is a reminder that a bridge is only as sound as the state of the chain it trusts. A single cache bug in the VM was enough to fake balances, and bridges with no outflow limits converted them into real ETH within minutes. When the full post-mortem lands, we will be watching for exactly what the cache key missed.&lt;/p&gt;

&lt;p&gt;Building on Move, running a bridge, or launching an appchain? &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;Contact Panda Security&lt;/a&gt; for a review of your VM integration and bridge controls, or see our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>move</category>
    </item>
  </channel>
</rss>
