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
USDCfrom Kamino and swapped it through Allbridge for ~949KUSDT(instructions 1-2). This first swap supplied theUSDTneeded for the following self-swaps and shifted both theUSDCandUSDTPool states.

- Step 2: The attacker submitted five ~100K
USDTself-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 distortedUSDTPool produced ~2.24MvUSD, which theUSDCPool converted into ~2.24MUSDC. The inflatedvUSDoutput 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
USDCKamino flash loan principal and the ~11.2USDCfee (instruction 9). The final attacker balances were ~1.12MUSDCand ~539KUSDT.
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
- [1] Allbridge Core, Technical Post-Mortem: Solana Pool Swap Exploit
- [2] Allbridge Core JS SDK, Solana Swap account interface (
bridge.json) - [3] Allbridge Core EVM Contracts, Pool accounting reference (
Pool.sol)
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 where (the secp256k1 curve order). The signing flow produces a commitment , a challenge , and a response , where is the private key. Once broadcast, is public.
The linear relation is the critical point. A correct implementation remains secure because every 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 , and use the resulting scalar as . 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 rather than .
This means 64 bits of every nonce are fixed to zero. From the Schnorr response equation , each signature gives a public modular linear relation where the result is bounded by . 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 , 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
- [1] Boneh and Venkatesan, Hardness of Computing the Most Significant Bits of Secret Keys (HNP)
- [2] Zilliqa Ledger Incident Hub
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.56NIGHT[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
amountandadaAmountwhile keeping the signed byte sequence unchanged. -
Step 4: The Cardano
TreasuryCheckvalidator parsed the redeemer as a high-value withdrawal and verified the reused signature successfully. The transaction paid 203,001,692.164714NIGHTto 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
- [1] BlockSec Phalcon's post on Wanchain exploit
- [2] Wanchain Cardano-BNB Chain Bridge Incident Post-Mortem
- [3] Wanchain status API record for representative BSC transaction
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 theETHprice 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), andregisterNewBondGroup([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 andexceptionCountreaches 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
USDCthrough the protocol's OTC pools (GeneralizedDotc), obtaining 532,144USDC. -
Step 5: The attacker repeated the same pattern against the second
BondMakerCollateralizedEthcontract, bringing the total profit to approximately 542,144.63USDC.

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.
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.
-
Official website: https://blocksec.com/
-
Official Twitter account: https://twitter.com/BlockSecTeam



