dYdX Callback Approval Drain
Exploit Transactions
Victim Addresses
0xbadc0defafcf6d4239bdf0b66da4d7bd36fcf05aEthereumLoss Breakdown
Similar Incidents
USDTStaking Approval Drain
43%Unibot Approval Drain
42%Helper Callback Drain
40%Flooring extMulticall Approval Drain
38%WETH Drain via Unprotected 0xfa461e33 Callback on 0x03f9-62c0
38%CivTrade Fake-Pool Callback Drain
37%Root Cause Analysis
dYdX Callback Approval Drain
1. Incident Overview TL;DR
An unprivileged attacker drained WETH from callback holder 0xbadc0defafcf6d4239bdf0b66da4d7bd36fcf05a by routing a dYdX SoloMargin ActionType.Call through an attacker-deployed helper contract. During the callback, the victim contract delegatecalled implementation 0xdd6bd08c29ff3ef8780bf6a10d8b620a93ac5705, which granted the helper a large WETH allowance. The attacker then used the helper's sweep function to transferFrom the victim's full WETH balance to the attacker EOA. The exploit required only public state, a self-deployed helper, and two normal Ethereum transactions.
2. Key Background
The relevant dYdX component is SoloMargin 0x1e0447b19bb6ecfdae1e4ae1694b0c3659614e4e. Its verified OperationImpl code parses ActionType.Call by setting the callee from args.otherAddress and then invoking ICallee(args.callee).callFunction(msg.sender, args.account, args.data). That means any contract reachable through a valid operate call can execute arbitrary callback logic on-chain.
function parseCallArgs(
Account.Info[] memory accounts,
ActionArgs memory args
) internal pure returns (CallArgs memory) {
assert(args.actionType == ActionType.Call);
return CallArgs({
account: accounts[args.accountId],
callee: args.otherAddress,
data: args.data
});
}
ICallee(args.callee).callFunction(
msg.sender,
args.account,
args.data
);
Source origin: dYdX SoloMargin OperationImpl verified source collected for 0x56e7d4520abfecf10b38368b00723d9bd3c21ee1.
Before the exploit, the callback holder already contained exploitable WETH. The ACT pre-state is block 15625423, immediately before block 15625424, with the attacker helper already deployed, the victim callback holding at least 1101308701043355841780 WETH, and the helper allowance set to zero.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is a permissionless approval-granting callback path in a custody-bearing contract. The victim callback holder 0xbadc0def... delegatecalls implementation 0xdd6bd08... when dYdX routes callFunction into it. In the observed execution, that implementation checks whether WETH allowance from the victim to the caller helper is zero and, if so, grants a very large approval to the helper. No access-control check binds the approved spender to the victim owner or any trusted operator. Once the approval exists, the attacker helper's cfdb5486(address,address) routine can read the victim balance and pull it with transferFrom. The broken invariant is straightforward: a contract that holds ERC20 balances must not grant spending authority to an attacker-chosen operator during a generic callback.
4. Detailed Root Cause Analysis
The adversary first deployed helper contract 0x6554ff0f2b6613bb2baa9a45788ad8574a805f6d in transaction 0x1886da396650f668999fd21b0ad269599b64193fd7eaba56cffbbdb68e712d69. Clustered sender history shows this was the attacker's nonce-0 deployment and that the same EOA later sent both exploit transactions.
In transaction 0x59ddcf5ee5c687af2cbf291c3ac63bf28316a8ecbb621d9f62d07fa8a5b8ef4e, the attacker called helper execute, which in turn called SoloMargin operate with ActionType.Call targeting the victim callback holder. The seed trace shows SoloMargin invoking 0xbadc0def...::callFunction, then delegatecalling implementation 0xdd6bd08...::callFunction. Inside that path, the implementation reads the current WETH allowance from the victim to the helper and then sets a large approval when the allowance is zero:
0xbaDc0dEf...::callFunction(...)
0xDd6Bd08c...::callFunction(...) [delegatecall]
WETH9::allowance(0xbaDc0dEf..., 0x6554FF0f...) -> 0
WETH9::approve(0x6554FF0f..., 56372227272130782805279000)
Trace origin: seed exploit trace for 0x59ddcf5ee5c687af2cbf291c3ac63bf28316a8ecbb621d9f62d07fa8a5b8ef4e.
That approval is the code-level breakpoint. It converts the victim's WETH custody into attacker-usable transfer authority without any privileged credential. The exploit conditions are minimal: the callback path must remain reachable through SoloMargin, the victim must hold WETH, and the helper allowance must start at zero so the approval branch executes.
Fourteen blocks later, transaction 0x631d206d49b930029197e5e57bbbb9a4da2eb00993560c77104cd9f4ae2d1a98 called helper selector 0xcfdb5486 with (source = 0xbadc0def..., token = WETH). The follow-up trace shows the helper reading the victim balance and calling WETH.transferFrom(source, attacker, balance). The receipt records the corresponding WETH Transfer event from the victim callback holder to attacker EOA 0xb9f78307ded12112c1f09c16009e03ef4ef16612.
0x6554FF0f...::cfdb5486(0xbaDc0dEf..., WETH)
WETH9::balanceOf(0xbaDc0dEf...) -> 1101650251927809373491
WETH9::transferFrom(
0xbaDc0dEf...,
0xB9F78307DEd12112c1f09C16009e03eF4ef16612,
1101650251927809373491
)
Trace origin: follow-up drain trace for 0x631d206d49b930029197e5e57bbbb9a4da2eb00993560c77104cd9f4ae2d1a98.
5. Adversary Flow Analysis
The adversary flow is a two-transaction ACT sequence.
- Helper deployment: attacker EOA
0xb9f78307...deploys helper0x6554ff0f..., which stores the deployer as owner and exposes anexecutewrapper for SoloMargin plus atransferFrom-based sweep primitive. - Approval induction: the attacker sends
0x59dd...to the helper, which calls SoloMarginoperateand routes a crafted callback into0xbadc0def.... The callback implementation approves the helper to spend WETH from the victim contract. - Profit realization: the attacker sends
0x631d...to the helper, invokingcfdb5486(source, token). The helper reads the victim WETH balance and pulls the full amount into the attacker EOA usingtransferFrom.
The identified adversary-controlled accounts are:
0xb9f78307ded12112c1f09c16009e03ef4ef16612as the attacker EOA.0x6554ff0f2b6613bb2baa9a45788ad8574a805f6das the attacker-deployed helper.
The victim-side addresses materially involved are:
0xbadc0defafcf6d4239bdf0b66da4d7bd36fcf05aas the vulnerable callback holder.0xf2faff35a744912ca103ea1c6662d1c91b6221e9as the callback owner candidate named in the analysis.
6. Impact & Losses
The direct loss is the full visible WETH balance held by 0xbadc0def... at the time of the sweep. The receipt and follow-up trace show 1101650251927809373491 wei of WETH, or 1101.650251927809373491 WETH, transferred to the attacker EOA. The attacker paid 8269228392229968 wei in total gas fees across the two exploit transactions, leaving a net ETH-equivalent gain of 1101641982699417143523 wei.
The measured loss item is:
- WETH: raw amount
"1101650251927809373491", decimals18
7. References
- Seed exploit trace: transaction
0x59ddcf5ee5c687af2cbf291c3ac63bf28316a8ecbb621d9f62d07fa8a5b8ef4e - Helper deployment metadata: transaction
0x1886da396650f668999fd21b0ad269599b64193fd7eaba56cffbbdb68e712d69 - Follow-up drain trace and receipt: transaction
0x631d206d49b930029197e5e57bbbb9a4da2eb00993560c77104cd9f4ae2d1a98 - dYdX SoloMargin verified source at
0x56e7d4520abfecf10b38368b00723d9bd3c21ee1 - Collected decompilation and bytecode artifacts for victim callback
0xbadc0def...and delegatecalled implementation0xdd6bd08...