Originally published at Avalanche Safe: A Stale Approval to an Empty Address
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.
What Happened
According to the security monitor Defimon, which first reported the incident, and an on-chain reconstruction published by BlockintAI, the sequence was:
-
7 March 2026, 16:10 UTC. The Safe executed a batch transaction that granted unlimited allowances for WAVAX, USDC, WETH.e and BTC.b to
0x3725bd4d175283108156c3f15f86e1c51266155d. - 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.
-
26 October 2026, 12:56 UTC, block 98,605,391. A fresh wallet,
0x4c48b79b97516719444056d2bf0dc9564138fa0a, funded about 27 minutes earlier with 6.77 AVAX, sent one Multicall3 transaction designed to claim the empty address and sweep the Safe. - In the same block, at position 0, MEV bot
0x36b112875f921e51f5ea8ff39b374960694d21b3ran 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 |
Root Cause: An Approval Is a Promise to an Address
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.
Why the Address Was Claimable
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:
- Deploy CreateCall on Avalanche through the singleton factory.
- Make 12 throwaway deployments through it to advance its nonce.
- Deploy a sweeper contract as the 13th, landing at
0x3725bd…155d. - Have the sweeper call
transferFromon 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.
A Simplified Model of the Sweep
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.
The Front-Run and the Bounty
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:
-
The operator moved funds on other chains. 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
0x70d54baf9ffa48b8fe4e843774a44e24ff452099, which it puts at about $38.7M. That is a sensible move: if one chain had an orphaned approval, others might too. - Address poisoning followed immediately. 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.
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.
Why This Pattern Keeps Coming Back
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:
- Pre-approving "future" deployments on a chain where the contract has not been deployed yet, so the deployment script can run later.
- Copy-pasting multisig transaction batches between chains and only changing the chain ID.
-
Counterfactual addresses derived from
CREATEwith a factory anyone can call, rather thanCREATE2with a salt and init code hash that only the intended bytecode can reach. - Treasury allowances that outlive the integration they were made for.
Lessons for Teams and Auditors
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.
Key Addresses and Transactions
- Victim Safe (Avalanche):
0x631fca…b9b8(shortened as in BlockintAI's report) - Approved address:
0x3725bd4d175283108156c3f15f86e1c51266155d - Attacker wallet:
0x4c48b79b97516719444056d2bf0dc9564138fa0a - MEV bot:
0x36b112875f921e51f5ea8ff39b374960694d21b3 - Bot holding wallet:
0x11eb9b677510e2594857f4d137fd12a7fef39bc5 - CreateCall:
0xb22d635d552ec95142e2abe3ffb859ea7d7c0316 - March 7 approvals:
0x6952f80b...42d3829e - Failed attacker transaction:
0xf1f87d37...4a331289 - MEV bot transaction:
0xf88a7960...3c007ed5 - Bounty agreement message:
0xd2916fec...76911afc
Sources
- Defimon Alerts on X: first report, the approved address, the 2-of-7 setup and the $7.45M figure
- Defimon Alerts on X, bot's on-chain message: the bot's offer to return funds
- BlockintAI research post: on-chain reconstruction, token amounts, CreateCall nonce analysis and return transfers
- CryptoTimes report: independent summary of the approvals, the front-run and the bounty negotiation
- Transaction receipts for the attacker and bot transactions, read by us from a public Avalanche C-Chain RPC node
Closing Thoughts
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.
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.
Top comments (0)