0x Settler BASIC Drain
Exploit Transactions
0x33b2cb5bc3c0ccb97f0cc21e231ecb6457df242710dfce8d1b68935f0e05773bVictim Addresses
0xdf31a70a21a1931e02033dbba7deace6c45cfd0fEthereumLoss Breakdown
Similar Incidents
USDTStaking Approval Drain
34%WBTC Drain via Insecure Router transferFrom Path
34%0x7CAE Approved-Spender Drain
33%Unibot Approval Drain
33%dYdX Callback Approval Drain
33%RubicProxy USDC Drain
32%Root Cause Analysis
0x Settler BASIC Drain
1. Incident Overview TL;DR
After victim EOA 0x382ffce2287252f930e1c8dc9328dac5bf282ba1 approved MainnetSettler at 0xdf31a70a21a1931e02033dbba7deace6c45cfd0f for unlimited ANDY, attacker EOA 0xc31a49d1c4c652af57cefdef248f3c55b801c649 used helper contract 0xf0d539955974b248d763d60c3663ef272dfc6971 to invoke execute() with a crafted BASIC action. That action caused Settler itself to call ANDY.transferFrom(victim, helper, 88438777696239504000000), after which the helper sold the stolen ANDY for ETH.
The root cause is that BASIC becomes an arbitrary external-call primitive when sellToken == address(0). In that branch, Settler performs a raw pool.call(data) without binding the token owner to the current caller, so any unrelated caller can spend direct ERC20 approvals previously granted to Settler.
2. Key Background
MainnetSettler is a public execution contract used by 0x Settler on Ethereum mainnet. Its execute(AllowedSlippage, bytes[] actions, bytes32) entrypoint is callable by any account.
The BASIC action is intended to support generic approval-and-swap integrations. Its payload decodes to (sellToken, bps, pool, offset, data) and then routes into basicSellToPool.
This incident depends on a direct ERC20 approval to Settler. The victim did not authorize the attacker through Permit2 or AllowanceHolder. Instead, the victim directly approved Settler on-chain in tx 0x8df54ebe76c09cda530f1fccb591166c716000ec95ee5cb37dff997b2ee269f2.
Approval tx trace summary
Andy::approve(
0xDf31A70a21A1931e02033dBBa7DEaCe6c45cfd0f,
type(uint256).max
)
emit Approval(
owner: 0x382fFCe2287252F930E1C8DC9328dac5BF282bA1,
spender: 0xDf31A70a21A1931e02033dBBa7DEaCe6c45cfd0f,
value: 115792089237316195423570985008687907853269984665640564039457584007913129639935
)
3. Vulnerability Analysis & Root Cause Summary
The vulnerable component is MainnetSettler's BASIC execution path. Verified Settler source shows that execute() is public and dispatches decoded actions without restricting callers. In basicSellToPool, the sellToken == address(0) branch skips token accounting and approval preparation, checks only that offset == 0, and then executes payable(pool).call{value: value}(data). Because pool and data are attacker-controlled and only a short restricted-target denylist is enforced, that branch is an arbitrary call primitive. If the chosen target is an ERC20 token and some victim has already approved Settler, the token sees msg.sender == MainnetSettler and honors transferFrom. That violates the core invariant that public Settler executions should only move assets already held by Settler or assets explicitly authorized by the active caller for that same execution.
function basicSellToPool(IERC20 sellToken, uint256 bps, address pool, uint256 offset, bytes memory data) internal {
if (_isRestrictedTarget(pool)) revert ConfusedDeputy();
...
} else if (address(sellToken) == address(0)) {
if (offset != 0) revert InvalidOffset();
} else {
...
}
(success, returnData) = payable(pool).call{value: value}(data);
}
function execute(AllowedSlippage calldata slippage, bytes[] calldata actions, bytes32) public payable returns (bool) {
...
_checkSlippageAndTransfer(slippage);
return true;
}
4. Detailed Root Cause Analysis
The exploit sequence starts from public state established by the victim's approval transaction. After block 23133706, victim 0x382ffce2287252f930e1c8dc9328dac5bf282ba1 had granted unlimited ANDY allowance to Settler and still held enough ANDY to be drained.
The attacker then submitted tx 0x33b2cb5bc3c0ccb97f0cc21e231ecb6457df242710dfce8d1b68935f0e05773b at block 23134257. The trace shows helper contract 0xf0d539955974b248d763d60c3663ef272dfc6971 calling:
0xDf31A70a21A1931e02033dBBa7DEaCe6c45cfd0f::execute(
slippage=(recipient=0x0, buyToken=0x0, minAmountOut=0),
actions=[
BASIC(
sellToken=address(0),
bps=10000,
pool=0x68BbEd6A47194EFf1CF514B50Ea91895597fc91E,
offset=0,
data=transferFrom(
0x382fFCe2287252F930E1C8DC9328dac5BF282bA1,
0xF0D539955974b248d763D60C3663eF272dfC6971,
88438777696239504000000
)
)
]
)
Settler then executed the arbitrary call into the ANDY token contract:
0x68BbEd6A47194EFf1CF514B50Ea91895597fc91E::transferFrom(
0x382fFCe2287252F930E1C8DC9328dac5BF282bA1,
0xF0D539955974b248d763D60C3663eF272dfC6971,
88438777696239504000000
)
The ANDY source confirms standard allowance semantics:
function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
_transfer(sender, recipient, amount);
uint256 currentAllowance = _allowances[sender][_msgSender()];
require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
unchecked {
_approve(sender, _msgSender(), currentAllowance - amount);
}
return true;
}
Because _msgSender() during that call was MainnetSettler, ANDY consumed the victim's allowance to Settler and transferred the tokens. The balance diff for the exploit transaction confirms that the victim lost 88438777696239504000000 ANDY and recipient 0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 later received the same amount during monetization. No victim signature, Permit2 witness, or AllowanceHolder path was required in the exploit transaction.
5. Adversary Flow Analysis
Stage 1 was passive observation. The attacker only needed public approval state and the victim's token balance after tx 0x8df54ebe76c09cda530f1fccb591166c716000ec95ee5cb37dff997b2ee269f2.
Stage 2 was unauthorized spending. The attacker EOA 0xc31a49d1c4c652af57cefdef248f3c55b801c649 triggered helper contract 0xf0d539955974b248d763d60c3663ef272dfc6971, which called MainnetSettler.execute() with a one-action BASIC payload targeting the ANDY token contract and embedding transferFrom(victim, helper, amount).
Stage 3 was monetization. The same helper approved the 0x token-approval contract, sold the stolen ANDY, unwrapped WETH, and returned ETH to the attacker EOA in the same trace. That confirms the helper contract was attacker-controlled but not privileged; it was only an execution convenience layered on top of a permissionless Settler call path.
6. Impact & Losses
The direct token loss evidenced on-chain is:
[
{
"token_symbol": "ANDY",
"amount": "88438777696239504000000",
"decimal": 18
}
]
This corresponds to 88438.777696239504 ANDY removed from the victim and then liquidated by the attacker. The incident demonstrates that any user who directly approves Settler can become drainable through the same public call path.
7. References
- Approval transaction:
0x8df54ebe76c09cda530f1fccb591166c716000ec95ee5cb37dff997b2ee269f2 - Exploit transaction:
0x33b2cb5bc3c0ccb97f0cc21e231ecb6457df242710dfce8d1b68935f0e05773b - MainnetSettler:
0xdf31a70a21a1931e02033dbba7deace6c45cfd0f - ANDY token:
0x68bbed6a47194eff1cf514b50ea91895597fc91e - Helper contract:
0xf0d539955974b248d763d60c3663ef272dfc6971 - Attacker EOA:
0xc31a49d1c4c652af57cefdef248f3c55b801c649 - Source evidence used:
- Approval metadata and trace from the collector seed for tx
0x8df54ebe76c09cda530f1fccb591166c716000ec95ee5cb37dff997b2ee269f2 - Exploit trace and balance diff from the collector seed for tx
0x33b2cb5bc3c0ccb97f0cc21e231ecb6457df242710dfce8d1b68935f0e05773b - Verified ANDY source from the collector seed
- Verified MainnetSettler source from Etherscan API for
0xdf31a70a21a1931e02033dbba7deace6c45cfd0f
- Approval metadata and trace from the collector seed for tx