On 26 October 2026, a 2-of-7 Safe multisig on Avalanche C-Chain holding roughly $7.45M 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.
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.
According to the security monitor Defimon, which first reported the incident, and an on-chain reconstruction published by BlockintAI, the sequence was:
0x3725bd4d175283108156c3f15f86e1c51266155d.0x4c48b79b97516719444056d2bf0dc9564138fa0a, funded about 27 minutes earlier with 6.77 AVAX, sent one Multicall3 transaction designed to claim the empty address and sweep the Safe.0x36b112875f921e51f5ea8ff39b374960694d21b3 ran a copy of that transaction. It succeeded. The attacker’s transaction, at position 2, reverted.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.
Per BlockintAI’s reading of the logs, the bot’s transaction moved these balances out of the Safe:
| Token | Amount |
|---|---|
| USDC | 3,090,123.58 |
| WAVAX | 350,375.86 |
| WETH.e | 181.98 |
| BTC.b | 10.79 |
ERC-20 approve 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 transferFrom up to the allowance.
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.
Contracts created with the CREATE opcode get an address derived only from the deployer’s address and the deployer’s nonce:
address = keccak256(rlp([deployer, nonce]))[12:]
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.
Both Defimon and BlockintAI identify the deployer behind 0x3725bd…155d on the other chains as Safe’s public CreateCall helper (0xb22d635d552ec95142e2abe3ffb859ea7d7c0316), 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 13th deployment lands at 0x3725bd…155d.
Before 26 October, CreateCall had never been deployed on Avalanche. So the path to the Safe’s allowances was open to anyone who would:
0x3725bd…155d.transferFrom on all four tokens, pulling the Safe’s full balances.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.
The code below is a simplified illustration of the mechanism, not the attacker’s or the bot’s actual bytecode.
// 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 < tokens.length; i++) {
uint256 bal = tokens[i].balanceOf(victim);
uint256 allowed = tokens[i].allowance(victim, address(this));
uint256 amt = bal < allowed ? bal : allowed;
if (amt > 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 < 12; i++) createCall.performCreate(0, dummy);
// Nonce 13 lands on the approved address.
createCall.performCreate(0, sweeper);
}
}
Nothing in this flow requires a signature from the Safe. The Safe’s earlier signatures are already on-chain, in the form of allowances.
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, 0x11eb9b677510e2594857f4d137fd12a7fef39bc5, and posted an on-chain message pointing to the original attack and promising to return the assets to 0x35c6aa248e9495E0572598fBfE0528aCbAC2807C, asking for a 5 to 10% bounty.
On 27 October, one of the Safe’s owner addresses agreed on-chain to let the bot keep 42,500 WAVAX (about $371K, 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.
Two side notes from the reports:
0x70d54baf9ffa48b8fe4e843774a44e24ff452099, which it puts at about $38.7M. That is a sensible move: if one chain had an orphaned approval, others might too.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.
We have written before about how cross-chain assumptions break, for example in the WAX bridge negative fee exploit, and how wallet-level logic can be replayed in ways its authors never expected in the Argent-style wallet recovery replay. This incident is a cleaner version of the same lesson: an assumption that holds on one chain was carried to another chain where it did not hold.
The same shape shows up in several places:
CREATE with a factory anyone can call, rather than CREATE2 with a salt and init code hash that only the intended bytecode can reach.1. Verify spender code before every approval, on that chain. Before a Safe signs an approve, 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.”
2. Do not rely on CREATE nonce alignment across chains. If you need the same address everywhere, deploy with CREATE2 (or CREATE3) from a factory where the salt is bound to your deployer, so nobody else can land a different contract at your address.
3. Deploy first, approve second. 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.
4. Make allowances exact and short-lived. 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.
5. Audit allowances periodically, across every chain. An allowance inventory per Safe, per chain, with an owner and an expiry, would have flagged this one months ago. Our smart contract security checklist covers operational controls like this alongside code review.
6. Treat multisig operations as code. A batch transaction is a deployment. It deserves review, simulation on the target chain and a diff against what was intended.
0x631fca…b9b8 (shortened as in BlockintAI’s report)0x3725bd4d175283108156c3f15f86e1c51266155d0x4c48b79b97516719444056d2bf0dc9564138fa0a0x36b112875f921e51f5ea8ff39b374960694d21b30x11eb9b677510e2594857f4d137fd12a7fef39bc50xb22d635d552ec95142e2abe3ffb859ea7d7c03160x6952f80b...42d3829e0xf1f87d37...4a3312890xf88a7960...3c007ed50xd2916fec...76911afcThis 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.
Running treasuries or contracts across several EVM chains? Contact Panda Security for a review of your deployment scripts, multisig operations and allowance hygiene, or see our audit pricing.
A MoveVM script-caching flaw let one account credit itself unbacked WETH, USDC and AUSD on Initia, and three bridge reserves paid out about $2.7M in real assets.
A zero-quorum DAO vote gave an attacker Yapeswap's feeToSetter role, and a fake Yearn registry then routed every pair's farmed reserves into attacker vaults.
Flash-loaned X tokens bought two of three X100 Senate seats, and one transaction later the Freezer treasury sold 400,000 X for about 36 USDT on Ethereum.
A missing sign check on max_fee in WAX's eth.token withdraw action let an attacker bridge out ~$90K in USDC, USDT, DAI, WETH and WBTC for free.