yvWETH Approval Arbitrary Command Drain
On Ethereum mainnet, transaction 0xebaaab69baa3cd2543eb80ecfb8e3ed226b9e5a6f5694891a8adf4edbcbd8107 succeeded at block 24981717 on 2026-04-28T23:01:11Z. The attacker deployed helper contracts and exploited an unauthenticated execute() batch-action function on contract 0x143a737bffc6414b61134f513ceed1a64390181a, which held a prior max allowance over the victim’s Yearn WETH Vault shares (yvWETH, 0xa258c4606ca8206d8aa700ce2143d7db854d168c). The execute() function (selector 0x49650044) exposes 8 action types including arbitrary low-level calls and token transfers with no access control, allowing the attacker to weaponize the contract’s approval to pull the victim’s yvWETH, redeem it through the vault, and forward the resulting WETH. The victim lost 384.667252984919210375 yvWETH, which redeemed into 429.210570004163139903 WETH/ETH proceeds for the attacker, estimated by the prompt at roughly $1M.
Root Cause
Vulnerable Contract
- Vulnerable approved spender:
0x143a737bffc6414b61134f513ceed1a64390181a. - Source status: unverified on Etherscan. Decompiled via Dedaub (see
0x143a737bffc6414b61134f513ceed1a64390181a/recovered.sol). The contract is a general-purpose batch action executor, not a yvWETH-specific vault wrapper. - Owner/victim relation:
eth_call owner()(0x8da5cb5b) at the pre-exploit block returned0x98289e90d6fc92a8769bc892d006a2baa7705afe. - Prior approval:
victim_approval_logs.jsonshows the victim approved0x143a...foruint256.maxyvWETH in tx0x52b4e5703a2d06cc6bf6842edb04961cafdf5357178813a5606e5ea9c1846faeat block24909652(2026-04-18T22:10:11Z).
Vulnerable Function
- Function:
execute(Action[] calldata actions)— selector0x49650044, resolved from Dedaub decompilation asexecute((uint8,bytes)[]). - Caller in exploit: attacker child contract
0xcc9be93051e8ad00a70eba3df2571a18f94d5856. - The function iterates over an array of
Actionstructs ({uint8 actionType, bytes data}), dispatching each to one of 8 operation types — with no access control check:
| Type | Operation | Risk |
|---|---|---|
| 0 | transferFrom(msg.sender, this, amount) | Pull tokens from caller |
| 1 | approve(token, spender, amount) | Set arbitrary approvals from the contract |
| 2 | target.call{value}(callData) | Arbitrary low-level call from contract context |
| 3 | transfer(token, receiver, amount) | Drain tokens held by the contract |
| 4 | Send ETH to receiver | Drain ETH held by the contract |
| 5 | ERC4626 deposit to address(this) | Deposit into vaults |
| 6 | ERC4626 deposit to any receiver | Deposit into vaults for any address |
| 7 | ERC4626 redeem to any receiver | Redeem vault shares for any address |
The critical actions are type 1 (arbitrary approvals) and type 2 (arbitrary calls from the contract’s context). Together they let any caller execute arbitrary logic as the contract — including calling transferFrom on tokens where the contract holds allowances.
Vulnerable Code
// Decompiled source via app.dedaub.com (Solidity 0.8.34)
contract Contract {
address private constant OWNER = 0x98289e90d6FC92A8769BC892D006A2BaA7705AFE;
struct Action {
uint8 actionType;
bytes data;
}
function execute(Action[] calldata actions) public payable { // <-- NO ACCESS CONTROL
for (uint256 i = 0; i < actions.length; i++) {
uint8 t = actions[i].actionType;
if (t == 0) {
// transferFrom(msg.sender, this, amount) — pull from caller
} else if (t == 1) {
// approve(token, spender, amount) — arbitrary approval
} else if (t == 2) {
// target.call{value}(callData) — ARBITRARY CALL FROM CONTRACT CONTEXT
(address target, uint256 value, bytes memory callData) =
abi.decode(actions[i].data, (address, uint256, bytes));
(bool ok, bytes memory ret) = target.call{value: value}(callData);
if (!ok) revert CallFailed(ret);
} else if (t == 3) {
// transfer(token, receiver, amount) — drain contract tokens
}
// ... types 4-7: ETH send, ERC4626 deposit/redeem
emit ActionExecuted(i, t);
}
}
}
The execute() function is fully public with no require(msg.sender == OWNER) guard. Action type 2 is the critical gadget: it allows any caller to make the contract execute an arbitrary low-level call to any target with any calldata. Since the contract was the approved spender for the victim’s yvWETH, the attacker used type 2 to call yvWETH.transferFrom(victim, contract, amount), which succeeded because msg.sender (the contract) had uint256.max allowance. The contract then used further actions to redeem the shares and forward the WETH proceeds.
Why It’s Vulnerable
Expected behavior: a contract that has been granted allowance over a user’s tokens must only spend those shares when authorized by that user/owner, or must constrain callable targets/functions to safe flows. The execute() function should gate access with require(msg.sender == OWNER) — the rescueERC20 and rescueETH functions already enforce this check, showing the contract author understood the need for access control.
Actual behavior: execute() has no access control at all. The attacker called it directly from an EOA-deployed child contract and supplied three actions:
- Action type 2:
yvWETH.transferFrom(victim, 0x143a..., 384.667... yvWETH)— weaponizes the contract’suint256.maxapproval to pull the victim’s shares. - Action type 2:
yvWETH.withdraw(384.667..., 0x143a..., 10000)— redeems shares into WETH (note:withdrawis not a native action type, so arbitrary call is used here too). - Action type 3:
WETH.transfer(attackerChild, 429.210... WETH)— sends proceeds to the attacker.
This is an access-control failure: the contract author gated rescueERC20/rescueETH but left execute() — a far more powerful function — completely open. The victim’s prior approval was not itself sufficient to transfer funds; the loss required an unauthenticated function that let any third party execute arbitrary calls from the approved spender’s context.
Broader Attack Surface
The contract is not yvWETH-specific. Any user who had approved 0x143a... for any token was vulnerable to the same drain. Action type 1 (approve) also lets any caller set new approvals from the contract, enabling recursive exploitation of additional approvals. Action type 2 is effectively an unrestricted proxy — any on-chain action the contract could take (governance votes, staking, swapping) was available to any caller.
Attack Execution
High-Level Flow
- The attacker EOA deployed helper
0x64c589f3ef894678e46af3b851aa08be3f40a674. - The helper deployed child/orchestrator
0xcc9be93051e8ad00a70eba3df2571a18f94d5856. - The child confirmed the victim’s yvWETH balance and max allowance to
0x143a...viaSTATICCALLs tobalanceOfandallowance. - The child called
0x143a...execute(Action[])(selector0x49650044) with a 3-action payload:- Action 0 (type 2 — arbitrary call):
yvWETH.transferFrom(victim, 0x143a..., 384.667...)— pulls the victim’s shares. - Action 1 (type 2 — arbitrary call):
yvWETH.withdraw(384.667..., 0x143a..., 10000)— redeems shares into WETH. - Action 2 (type 3 — token transfer):
WETH.transfer(attackerChild, 429.210...)— sends proceeds to attacker.
- Action 0 (type 2 — arbitrary call):
- During the vault withdrawal, Yearn strategies unwound positions (including stETH→ETH via Curve pool
0xdc24316b...), wrapping to WETH. - The child unwrapped WETH to ETH and forwarded it to the helper, which forwarded it to the attacker EOA.
Detailed Call Trace
- Root: attacker EOA
0x6a818c673b098621e9bfb2adc80060906cf7b327CREATEs helper0x64c589f3ef894678e46af3b851aa08be3f40a674. - Path
.0: helperCREATEs child0xcc9be93051e8ad00a70eba3df2571a18f94d5856. - Path
.0.0: childSTATICCALLs yvWETHbalanceOf(address)(0x70a08231) for victim0x98289e...; output384667252984919210375. - Path
.0.1: childSTATICCALLs yvWETHallowance(address,address)(0xdd62ed3e) for(victim, 0x143a...); output isuint256.max. - Path
.0.2: child -> vulnerable0x143a...,CALL, selector0x49650044(execute(Action[])).- Path
.0.2.0(action type 2 — arbitrary call): vulnerable -> yvWETH,transferFrom(address,address,uint256)(0x23b872dd) from victim to vulnerable for384.667252984919210375yvWETH. - Path
.0.2.1(action type 2 — arbitrary call): vulnerable -> yvWETH,withdraw(uint256,address,uint256)(0xe63697c8) with shares384.667252984919210375, recipient0x143a..., andmaxLoss = 10000. - During the yvWETH redemption, Yearn strategies withdrew assets, including
0x85907b1a...and0x740e59f1...; the latter exchanged stETH for ETH through Curve pool0xdc24316b...and wrapped ETH to WETH. - Path
.0.2.1.0.11: yvWETH -> WETH,transfer(0x143a..., 429.210570004163139885). - Path
.0.2.2(action type 2 — arbitrary call, internal to vault withdrawal): not visible as a top-level sub-call of.0.2since it’s nested within.0.2.1’s Yearn strategy unwinding. - Path
.0.2.3(action type 3 — token transfer): vulnerable -> WETH,transfer(0xcc9b..., 429.210570004163139903).
- Path
- Path
.0.4: child -> WETH,withdraw(uint256)(0x2e1a7d4d) for429.210570004163139903WETH. - Path
.0.5: child forwards429.210570004163139903ETH to helper. - Path
.1: helper forwards429.210570004163139903ETH to attacker EOA.
Financial Impact
The victim-side loss was 384.667252984919210375 yvWETH from 0x98289e90d6fc92a8769bc892d006a2baa7705afe. The exploit redeemed that position into 429.210570004163139903 WETH/ETH proceeds, and the attacker EOA received 429.210570004163139903 ETH. The prompt estimates the position at roughly $1M; this report records the exact on-chain token/ETH quantities and does not rely on a live USD oracle.
Gas cost was 709,674 gas at 2.3307872 gwei, or approximately 0.0016540990753728 ETH. Net of gas, the attacker retained about 429.208915905087767103 ETH.
Evidence
- Exploit receipt status:
0x1. - Contract created in exploit receipt: helper
0x64c589f3ef894678e46af3b851aa08be3f40a674. - Prior approval:
Approval(victim, 0x143a..., uint256.max)in tx0x52b4e5703a2d06cc6bf6842edb04961cafdf5357178813a5606e5ea9c1846fae, block24909652. - Receipt log
274: yvWETH transfer from victim0x98289e...to0x143a..., amount384667252984919210375. - Receipt log
285: yvWETH burn from0x143a..., amount384667252984919210375. - Receipt log
286: WETH transfer from yvWETH vault to0x143a..., amount429210570004163139885. - Receipt log
288: WETH transfer from0x143a...to attacker child0xcc9b..., amount429210570004163139903. - Receipt log
290: WETHWithdrawal(address,uint256)by attacker child for429210570004163139903WETH. - Selector verification:
transferFrom(address,address,uint256) = 0x23b872dd,withdraw(uint256,address,uint256) = 0xe63697c8,transfer(address,uint256) = 0xa9059cbb,withdraw(uint256) = 0x2e1a7d4d,allowance(address,address) = 0xdd62ed3e, andbalanceOf(address) = 0x70a08231. - Exploit entry point: selector
0x49650044resolves toexecute((uint8,bytes)[])per Dedaub decompilation of0x143a.... The contract is a general-purpose batch action executor with no access control onexecute(). - Event
ActionExecuted(uint256, uint8)(selector0x337596d5) was emitted by the contract after each action in the batch, confirming the action type dispatch.
Related URLs
- Exploit transaction: https://etherscan.io/tx/0xebaaab69baa3cd2543eb80ecfb8e3ed226b9e5a6f5694891a8adf4edbcbd8107
- Vulnerable contract: https://etherscan.io/address/0x143a737bffc6414b61134f513ceed1a64390181a
- yvWETH token/vault: https://etherscan.io/address/0xa258c4606ca8206d8aa700ce2143d7db854d168c
- Victim approval tx: https://etherscan.io/tx/0x52b4e5703a2d06cc6bf6842edb04961cafdf5357178813a5606e5ea9c1846fae