Back to Blog

~$39.5M Lost: Allbridge, Wanchain & More | BlockSec Weekly

Code Auditing
July 30, 2026
18 min read
Key Insights

During the past week (2026/07/20 - 2026/07/26), the following 8 notable security incidents are featured, involving approximately $39.5M in total losses.

Date Incident Type Estimated Loss
2026/07/19* Allbridge Flawed Input Validation ~$1.65M
2026/07/20 Zilliqa Flawed Nonce Generation ~$400K
2026/07/20 Wanchain Flawed Message Encoding ~$500K
2026/07/22 42DAO Private Key Compromise ~$900K
2026/07/22 AFX Trade Private Key Compromise ~$24.15M
2026/07/22 B² Network Private Key Compromise ~$3.8M
2026/07/23 Verus Private Key Compromise ~$7.6M
2026/07/24 Lien Finance Flawed Validation Logic ~$542K

The Allbridge incident occurred on July 19 (17:51 UTC) and was not covered in last week's report. It is included here for completeness.

  • Allbridge was selected because it demonstrates an account-aliasing vulnerability enabled by Solana's positional account model. The vulnerability had to be reconstructed from the deployed program binary because the source code was unavailable.
  • Zilliqa was selected because it exposed a long-undetected vulnerability in an off-chain wallet implementation that had put users of legacy native ZIL accounts at risk of private-key compromise since 2019.
  • Wanchain was selected because it demonstrates how a bridge can be drained without compromising cryptography. The signatures were valid and correctly verified, yet a non-injective, delimiter-free message encoding allowed an authorization for 3,097.56 tokens to be reinterpreted on Cardano as 203,001,692.164714 tokens.
  • Lien Finance was selected because it illustrates how validation that balances match counts without checking the identity and multiplicity of the matched items can be circumvented.

Best Security Auditor for Web3

Validate design, code, and business logic before launch

Weekly Highlight: Allbridge Core

This incident is highlighted because the account-aliasing pattern applies to any Solana program that accepts the same mutable account in two instruction roles. The underlying mechanism, two mutable views of the same state where the second write silently overwrites the first, is the same structure as the classic ERC-20 self-transfer bug (transferFrom where from == to). The project's post-mortem [1] provides a high-level summary but does not detail the code-level mechanism. The analysis below was reconstructed from the deployed program binary to fill that gap.

On July 19, 2026 (17:51 UTC), Allbridge Core, a cross-chain bridge protocol on Solana, was exploited for approximately $1.65M (~$1.12M in USDC and ~$539K in USDT). The root cause was that the swap instruction accepted the same Pool account in both the send and receive roles without enforcing that the two are different. When the same account was passed twice, internal accounting updates from one role were silently overwritten by the other, while the actual token transfers had already settled. Five self-swaps distorted the Pool's recorded state enough for the attacker to convert a small USDT input into ~$2.24M USDC.

Background

Allbridge Core is a cross-chain bridge that routes swaps through an internal accounting unit called vUSD. A swap consists of two accounting legs:

source token -- swap_to_v_usd(send_pool) --> vUSD
vUSD        -- swap_from_v_usd(receive_pool) --> destination token

vUSD is not an SPL token. It exists only as a field inside each Pool's on-chain data. Each Pool tracks token_balance, v_usd_balance, and reserves. The send leg transfers the source token from the user to the send bridge vault, increases the send Pool's token-side accounting, and calculates the vUSD output. The receive leg adds that vUSD to the receive Pool, decreases its token-side accounting, and transfers the destination token from the receive bridge vault to the user.

Solana programs receive their accounts as a positional array from the caller. The program's instruction handler specifies which array positions correspond to which roles (send_pool, receive_pool, send_mint, etc.), but the runtime does not prevent a caller from passing the same account address in two positions. When the program deserializes accounts at two different positions, each deserialization produces a separate in-memory object. Mutations to one object do not affect the other. If the same account is passed twice, both objects start from the same on-chain data, but they diverge as soon as either is mutated.

When the handler finishes, the program's exit logic (e.g., Anchor's AccountsExit) serializes each account object back to the account's data in a fixed order. If two objects map to the same account, the second serialization overwrites the first completely.

Vulnerability Analysis

The exact source code for the affected Allbridge Core deployment was unavailable. The analysis was reconstructed from the 1,770,736-byte ELF stored in ProgramData (SHA-256: 40f776...346bb6). The last deployment slot (204,727,029) precedes the attack slot (433,941,722).

The buggy program is BrdgN2...ceWB. In the attack transaction's Swap instructions (instructions 3 through 7), the four send and receive account pairs were identical:

Roles Account
send_mint / receive_mint Es9vMF...wNYB
send_pool / receive_pool DW4a2E...wCX
send_bridge_token / receive_bridge_token 2xY9TD...vohV
send_user_token / receive_user_token 817UdW...CVct

The parser contains 16 recovered 32-byte comparisons that bind each role to its expected mint, vault, owner, PDA, authority, or Token program. However, no comparison enforces send_pool.key() != receive_pool.key(). The two Pool roles are deserialized independently before the handler runs.

The deployed ELF shows two Pool constructions, the send and receive calculations, and two Pool exits in a fixed order:

L54249-L54252  function_9881(accounts[5])     -> send_pool local
L54294-L54297  function_9881(accounts[6])     -> receive_pool local

L74177-L74195  function_12623(send_pool)      -> swap_to_v_usd
L74368-L74389  function_12940(receive_pool)   -> swap_from_v_usd

L55932-L55936  function_8300(send_pool)       -> full Pool serialization
L55940-L55944  function_8300(receive_pool)    -> full Pool serialization

Each function_9881 call allocates a separate 176-byte local Pool object and copies the decoded fields into it. When the account keys are equal, both objects start from the same data, but a mutation of the send object does not change the receive object.

Reconstructed pseudo-Rust (deployed behavior, not recovered source) [2] [3]:

let mut send_pool = Account::<Pool>::try_from(&accounts[5])?;
let mut receive_pool = Account::<Pool>::try_from(&accounts[6])?;

require_keys_eq!(send_pool.mint, send_mint.key());
require_keys_eq!(receive_pool.mint, receive_mint.key());

// Vault, owner, PDA, authority, and Token program bindings are checked.
// MISSING: require_keys_neq!(send_pool.key(), receive_pool.key());

token::transfer(send_user_token, send_bridge_token, amount)?;
let v_usd = swap_to_v_usd(&mut send_pool, amount)?;
let receive_amount =
    swap_from_v_usd(&mut receive_pool, v_usd, receive_amount_min)?;
token::transfer(receive_bridge_token, receive_user_token, receive_amount)?;

// AccountsExit after the handler returns:
send_pool.exit(program_id)?;     // first write
receive_pool.exit(program_id)?;  // second write, overwrites the first

function_8300 starts its writer at position 0 and serializes all 131 defined Pool bytes. The first exit writes send_pool; the second exit writes receive_pool over the same bytes. The final account state is therefore the stale receive-side value, not a merge of the two local updates.

The SPL token transfers are separate CPIs. They update vault accounts owned by the SPL Token program before the Pool exits, so the second Pool serialization cannot reverse the input transfer. Each aliased swap therefore leaves the transferred tokens in the vault while discarding the corresponding send-side Pool update. The receive-side update remains and moves the recorded Pool state toward higher v_usd_balance and lower token-side accounting.

The core flaw is that the program does not check that send_pool and receive_pool are different accounts. All other validations (mint binding, vault ownership, PDA derivation) pass because both roles legitimately belong to the same Pool.

Attack Analysis

The following analysis is based on the transaction 3LNLaG...Y39Q.

The exploit was completed in one transaction containing a Kamino flash borrow, seven Allbridge Swap instructions, and a Kamino repayment.

  • Step 1: The attacker borrowed ~1.12M USDC from Kamino and swapped it through Allbridge for ~949K USDT (instructions 1-2). This first swap supplied the USDT needed for the following self-swaps and shifted both the USDC and USDT Pool states.
  • Step 2: The attacker submitted five ~100K USDT self-swaps using the identical mint, Pool, vault, and user token accounts for both send and receive (instructions 3-7). Because the receive Pool was serialized last on every call, the persisted accounting recorded the receive-side reduction but not the send-side addition. The observed outputs decreased on each call as the distortion accumulated:
Self-swap Input Logged vUSD Price (vUSD/USDT) USDT output
1 ~100K ~162K ~5.24 ~47.8K
2 ~100K ~256K ~16.6 ~25.4K
3 ~100K ~471K ~57.2 ~12.5K
4 ~100K ~918K ~190 ~5.73K
5 ~100K ~1.82M ~563 ~2.36K

The five calls transferred ~500K USDT into the vault and returned ~93.8K USDT. The Pool's recorded token_balance dropped with each call while the real vault balance grew, widening the gap that the next step exploits.

  • Step 3: The attacker supplied only ~3.99K USDT (instruction 8). The distorted USDT Pool produced ~2.24M vUSD, which the USDC Pool converted into ~2.24M USDC. The inflated vUSD output was possible because the Pool's recorded token balance was far below its actual vault balance after five rounds of discarded send-side updates.
  • Step 4: The attacker repaid the ~1.12M USDC Kamino flash loan principal and the ~11.2 USDC fee (instruction 9). The final attacker balances were ~1.12M USDC and ~539K USDT.

Conclusion

The root cause of this incident was the missing validation that send_pool and receive_pool refer to different accounts. A single mutable Pool was deserialized into two local accounting objects, and the second full serialization overwrote the first after the Token program had already settled the vault transfers. This separated the Pool's recorded accounting from its real vault balance. The account aliases, ELF control flow, serialization order, transaction logs, and vault balances all support the same mechanism [1].

For Solana programs, any instruction that accepts the same account type in two or more mutable roles should either enforce uniqueness (require_keys_neq!) or merge mutations before exit. The Anchor framework's #[account] constraint system does not enforce cross-role uniqueness by default, so this check must be added explicitly. The general pattern, two mutable views of the same state where the second write silently overwrites the first, can appear wherever programs manage their own serialization order.

References

Get Started with Phalcon Explorer

Dive into Transactions to Act Wisely

Try now for free

More Incidents This Week

Zilliqa Ledger Wallet

On July 20, 2026, Zilliqa observed on-chain activity consistent with active exploitation of legacy native ZIL accounts, with known losses of approximately $400K. The root cause was a biased nonce in the Zilliqa Ledger app's EC-Schnorr signing path: the nonce-generation code copied the wrong 32 bytes from a 40-byte buffer after modular reduction, fixing the most significant 64 bits to zero. With several public signatures from the same account, an attacker could recover the private key through lattice-based techniques and drain the account.

Background

Zilliqa native transactions use EC-Schnorr signatures over secp256k1. For each signature, the signer must sample a fresh, full-width, unpredictable ephemeral nonce kk where 0k<N0 \le k < N (the secp256k1 curve order). The signing flow produces a commitment Q=compress(kG)Q = \text{compress}(kG), a challenge r=SHA256(QpublicKeymsg)Nr = \text{SHA256}(Q \| \text{publicKey} \| \text{msg}) \bmod N, and a response s=krxNs = k - r \cdot x \bmod N, where xx is the private key. Once broadcast, (r,s)(r, s) is public.

The linear relation s=krxNs = k - rx \bmod N is the critical point. A correct implementation remains secure because every kk is fresh and uniformly distributed. If the nonce is biased or too small, each public signature leaks information about the same private key.

Vulnerability Analysis

The relevant nonce-generation path was introduced in the Zilliqa Ledger app commit "Bug fixes based on Ledger team review". The intended flow was: generate 40 bytes of randomness, reduce modulo the curve order NN, and use the resulting scalar as kk. Generating a wide random integer and reducing it modulo the curve order is not a problem by itself. The flaw appeared during the copy into the nonce buffer:

unsigned char nonce[size+8];
cx_rng(nonce, size+8);
cx_math_modm(nonce, size+8, (unsigned WIDE char *) PIC(domain->n), size);
os_memcpy(T->K, nonce, size);

After cx_math_modm, the 32-byte scalar result sits right-aligned within the 40-byte buffer. os_memcpy(T->K, nonce, size) copies the first size (32) bytes, which retains the leading 8 zero-padding bytes and discards the last 8 bytes of entropy. The generated nonce therefore satisfies k<2192k < 2^{192} rather than k<N2256k < N \approx 2^{256}.

This means 64 bits of every nonce are fixed to zero. From the Schnorr response equation ki=si+rixNk_i = s_i + r_i x \bmod N, each signature gives a public modular linear relation where the result is bounded by 21922^{192}. This is a Hidden Number Problem (HNP) instance. With approximately four or more affected signatures from the same key, a standard lattice reduction algorithm recovers the private key in seconds on commodity hardware [1].

The defect was present in every released version of the Zilliqa Ledger app from 2019 until the incident was identified in July 2026 [2].

No Attack Analysis is provided for this incident. The exploitation was performed entirely off-chain: the attacker recovered private keys from publicly available on-chain signatures using lattice reduction, then signed standard transfer transactions. There is no multi-step on-chain attack sequence to analyze.

Conclusion

This incident was caused by an off-chain signing implementation flaw, not by an on-chain smart contract bug. The Zilliqa Ledger app produced EC-Schnorr signatures with nonces constrained to k<2192k < 2^{192}, leaking enough structured information for private-key recovery after several native transactions from the same account.

The primary mitigation is key retirement, not merely updating the Ledger app. A corrected app prevents new weakened signatures, but it cannot erase the already-public signatures recorded on-chain. Any affected key that has produced enough vulnerable signatures must be treated as compromised [2].

For wallet and protocol teams: treat nonce generation and encoding as security-critical code, prefer deterministic nonce generation following a well-reviewed standard where applicable, and for affected users, provide a coordinated migration path that accounts for front-running by attackers who may already hold the same private key.

References


Wanchain Cardano Bridge

On July 20, 2026, the Wanchain Cardano bridge lost approximately 515.2M NIGHT (~$500K) due to a non-injective message encoding in the Cardano TreasuryCheck Plutus validator [1]. The project's post-mortem [2] describes the high-level mechanism but does not provide byte-level encoding details or code; the analysis below reconstructs the full collision. Bridge-node signatures were valid and correctly verified, but the delimiter-free concatenation of variable-length fields allowed the attacker to shift byte boundaries between two adjacent numeric fields, reinterpreting an authorization for 3,097.56 NIGHT as a withdrawal of 203,001,692.164714 NIGHT.

Background

The affected component is Wanchain's Cardano cross-chain treasury system. A source-chain user burns or locks tokens, bridge nodes sign an authorization message constructed from the parsed redeemer fields, and the signed proof is submitted to the Cardano TreasuryCheck Plutus validator to release assets from the treasury.

Vulnerability Analysis

The TreasuryCheck validator verifies the bridge-node signature over a raw concatenation of 14 parsed redeemer fields:

hashRedeemer = sha3_256 $ mconcat
  [ toPkhPay, toPkhStk, policy, assetName
  , packInteger amount, packInteger adaAmount
  , txHash, packInteger index, packInteger mode
  , uniqueId, packInteger txType, packInteger ttl
  , packInteger outputCount, userData
  ]

The integer encoding used by packInteger is variable-length, and the concatenation has no length prefix, type separator, or domain-separated typed serialization. Different semantic tuples can therefore produce the same signed byte string.

In the representative attack, bridge nodes signed a normal withdrawal:

amount    = 3097560000     -> packInteger = b8a103c0
adaAmount = 1206800        -> packInteger = 126a10
combined                                  = b8a103c0126a10

The attacker submitted a Cardano redeemer parsed as:

amount    = 203001692164714 -> packInteger = b8a103c0126a
adaAmount = 16              -> packInteger = 10
combined                                   = b8a103c0126a10

The byte sequences are identical. The signature check passed, and the Cardano contract interpreted the redeemer as a withdrawal roughly 65,000 times larger than authorized.

Attack Analysis

The following analysis references the representative Cardano transaction 0a4861...2ea1 and the BSC source transaction 0xe90111...d5f26b.

  • Step 1: The attacker created normal-looking source-chain bridge requests. For the representative case, the BSC transaction burned 3,110 NIGHT, and the Wanchain API recorded an expected Cardano receive amount of 3,097.56 NIGHT [3].

  • Step 2: Bridge nodes signed the authorization hash constructed from the raw, concatenated fields.

  • Step 3: The attacker changed the Cardano redeemer boundaries between amount and adaAmount while keeping the signed byte sequence unchanged.

  • Step 4: The Cardano TreasuryCheck validator parsed the redeemer as a high-value withdrawal and verified the reused signature successfully. The transaction paid 203,001,692.164714 NIGHT to the attacker.

  • Step 5: The same pattern was repeated across multiple Cardano transactions, resulting in a total drain of approximately 515,206,545.426856 NIGHT (~$500K).

Conclusion

No cryptography was broken. The signers produced valid signatures, and the TreasuryCheck validator verified them correctly. The flaw was in the message construction: hashRedeemer folded 14 variable-length fields into one byte string with no delimiter or length prefix, making the encoding non-injective. Different field tuples produce the same preimage, the same hash, and the same valid signature.

The fix is to make the signed encoding injective so that exactly one field tuple can produce any given byte string. Fixed-width integer encoding or length-prefixing each field pins down the boundaries the attacker moved. A standard structured encoder achieves the same result and is harder to get wrong. The general rule: sign a canonical serialization rather than a raw concatenation. This is the same class of bug as Solidity's abi.encodePacked with variable-length arguments, where different input tuples can produce identical byte sequences. It applies to any signed message assembled from variable-length fields, and is especially relevant for bridges where the parties constructing and parsing the message operate in different systems.

References


Lien Finance

On July 24, 2026, Lien Finance, a decentralized bond OTC protocol on Ethereum, was exploited for approximately $542K in USDC. The root cause was a validation flaw in the bond exchange function: it checked that the total number of shared bonds matched between input and output groups, but did not track which specific bonds were matched. This allowed the attacker to bypass the burn step for all input bonds while minting an uncollateralized bond, which was then sold for real USDC through the protocol's OTC pools.

Background

Lien Finance is a DeFi protocol that issues collateralized bonds against ETH. Its BondMakerCollateralizedEth contract lets users lock ETH as collateral to mint bond tokens. A bond's payoff is defined by an fnMap, a piecewise-linear function mapping the collateral price at maturity to the amount that bond pays out.

The meaningful unit is the bond group. registerNewBondGroup() verifies that the payoffs of all bonds in a group sum to the ETH price at every breakpoint of their combined fnMap. A group satisfying that condition reconstitutes exactly one unit of collateral. This is why groups are interchangeable: any two that satisfy the condition are worth the same thing, and the exchangeEquivalentBonds() conversion path relies on that guarantee.

Bond and group registration are both permissionless: any address can register a new bond by supplying a maturity and an arbitrary fnMap, and any address can register any list of already-registered bond IDs as a group, subject only to the payoff-sum check.

Vulnerability Analysis

The buggy contracts are 0xDA6F...BEf0 and 0x8432...7de0.

When a bond appears in both the input and the output group, burning it and immediately re-minting it is wasted work. The exceptionBonds parameter names those bonds so the exchange can skip both steps. The function enforces this with a single counter exceptionCount: it increments once per match while scanning the input group and decrements once per match while scanning the output group, never recording which bondID matched.

This means the input group [BondA, BondB] and the output group [BondC, BondA, BondA], with exceptionBonds = [BondA, BondB], pass validation. The counter reaches 2 on the input side (one match each for BondA and BondB) and returns to 0 on the output side, but both decrements come from BondA's two entries. The exchange burns nothing and mints only BondC: output-group tokens created with no input consumed.

Attack Analysis

The attack is split across two transactions: Step 1 occurs in 0xe8689a...284d0f, and Steps 2-5 occur in 0xb96d57...48e0e7.

  • Step 1: The attacker called the permissionless registerNewBond() three times to define BondA, BondB, and BondC, sharing the same maturity. BondA and BondB are leveraged calls splitting the collateral evenly below $3,200, where the payoffs sum to the ETH price as _assertBondGroup() requires. BondC is a deep out-of-the-money LBT struck at $3,200 against $1,870 spot: zero intrinsic value, but still roughly $7.64/IMT of time value as an option.

  • Step 2: The attacker registered two bond groups. registerNewBondGroup([BondA, BondB]) returned Group 36 (input), and registerNewBondGroup([BondC, BondA, BondA]) returned Group 37 (output). BondC's flat-zero payoff adds nothing to the breakpoint sums, so its presence does not disturb the equivalence condition. BondA is listed twice in Group 37, which is what makes the exchange pass validation.

  • Step 3: The attacker called exchangeEquivalentBonds(inputGroup = 36, outputGroup = 37, amount = 6968565865905, exceptionBonds = [BondA, BondB]). Scanning the input group: BondA and BondB each match an exception, so both skip the burn and exceptionCount reaches 2. Scanning the output group: BondC matches nothing and is minted, then BondA's two entries each match an exception and decrement the counter to 0.

  • Step 4: The attacker sold the minted BondC for USDC through the protocol's OTC pools (GeneralizedDotc), obtaining 532,144 USDC.

  • Step 5: The attacker repeated the same pattern against the second BondMakerCollateralizedEth contract, bringing the total profit to approximately 542,144.63 USDC.

Conclusion

The exceptionBonds mechanism, meant to skip re-minting of shared bonds, could be applied to every input bond at once by duplicating a bondID in the output group. This broke the invariant that bond tokens are only created against deposited collateral via issueNewBonds(). The fix is to track which specific bondIDs have been matched (e.g., using a bitmap or set), ensuring each exception is consumed exactly once in both groups.

Get Started with Phalcon Security

Detect every threat, alert what matters, and block attacks.

Try now for free

About BlockSec

BlockSec is a full-stack blockchain security and crypto compliance provider. We build products and services that help customers to perform code audit (including smart contracts, blockchain and wallets), intercept attacks in real time, analyze incidents, trace illicit funds, and meet AML/CFT obligations, across the full lifecycle of protocols and platforms.

BlockSec has published multiple blockchain security papers in prestigious conferences, reported several zero-day attacks of DeFi applications, blocked multiple hacks to rescue more than 20 million dollars, and secured billions of cryptocurrencies.

Sign up for the latest updates
~$135萬美元損失:BarnBridge、DeFiTuna | BlockSec 週報
Security Insights

~$135萬美元損失:BarnBridge、DeFiTuna | BlockSec 週報

本週報告涵蓋2026年7月13日至19日的2起安全事件,以太坊和Solana總損失約135萬美元。Solana借貸協議DeFiTuna損失約57萬美元,原因是倉位健康檢查將零值倉位視為健康而忽略未償債務,攻擊者利用可控的交換路由和低流動性池觸發此缺陷。BarnBridge損失約77.6萬美元,攻擊者利用以太坊上已棄用但仍活躍的治理系統通過惡意提案並竊取用戶授權的USDC。

~$800K損失:Hinkal雙重支付事件 | BlockSec週報
Security Insights

~$800K損失:Hinkal雙重支付事件 | BlockSec週報

本週安全報告涵蓋2026年6月29日至7月5日1起重大事件,以太坊損失約80萬美元。Hinkal隱私池協議遭雙重花費攻擊,攻擊者疑似利用舊版票據格式漏洞,從單筆存款衍生出多個無效化符號。報告分析了可能的電路層漏洞、攻擊流程及對基於無效化符號的隱私協議的廣泛影響。

電子報 - 2026年6月
Security Insights

電子報 - 2026年6月

本月報告涵蓋2026年6月三起最大安全事件,確認損失合計約2200萬美元。一次精密蜜罐攻擊利用未受檢查的代幣授權,從JaredFromSubway的MEV機器人抽走約1500萬美元。兩個舊版Aztec rollup部署因證明結算邊界漏洞損失約435萬美元。SecondFi的Ed25519實現缺陷暴露錢包私鑰,導致374個錢包共損失約240萬美元。三起事件共同模式:表面完整但從未真正執行的安全保證。

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit