yvWETH Approval Arbitrary Command Drain
2026-4-27 23:59:13 Author: www.darknavy.org(查看原文) 阅读量:0 收藏

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 returned 0x98289e90d6fc92a8769bc892d006a2baa7705afe.
  • Prior approval: victim_approval_logs.json shows the victim approved 0x143a... for uint256.max yvWETH in tx 0x52b4e5703a2d06cc6bf6842edb04961cafdf5357178813a5606e5ea9c1846fae at block 24909652 (2026-04-18T22:10:11Z).

Vulnerable Function

  • Function: execute(Action[] calldata actions) — selector 0x49650044, resolved from Dedaub decompilation as execute((uint8,bytes)[]).
  • Caller in exploit: attacker child contract 0xcc9be93051e8ad00a70eba3df2571a18f94d5856.
  • The function iterates over an array of Action structs ({uint8 actionType, bytes data}), dispatching each to one of 8 operation types — with no access control check:
TypeOperationRisk
0transferFrom(msg.sender, this, amount)Pull tokens from caller
1approve(token, spender, amount)Set arbitrary approvals from the contract
2target.call{value}(callData)Arbitrary low-level call from contract context
3transfer(token, receiver, amount)Drain tokens held by the contract
4Send ETH to receiverDrain ETH held by the contract
5ERC4626 deposit to address(this)Deposit into vaults
6ERC4626 deposit to any receiverDeposit into vaults for any address
7ERC4626 redeem to any receiverRedeem 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:

  1. Action type 2: yvWETH.transferFrom(victim, 0x143a..., 384.667... yvWETH) — weaponizes the contract’s uint256.max approval to pull the victim’s shares.
  2. Action type 2: yvWETH.withdraw(384.667..., 0x143a..., 10000) — redeems shares into WETH (note: withdraw is not a native action type, so arbitrary call is used here too).
  3. 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

  1. The attacker EOA deployed helper 0x64c589f3ef894678e46af3b851aa08be3f40a674.
  2. The helper deployed child/orchestrator 0xcc9be93051e8ad00a70eba3df2571a18f94d5856.
  3. The child confirmed the victim’s yvWETH balance and max allowance to 0x143a... via STATICCALLs to balanceOf and allowance.
  4. The child called 0x143a... execute(Action[]) (selector 0x49650044) 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.
  5. During the vault withdrawal, Yearn strategies unwound positions (including stETH→ETH via Curve pool 0xdc24316b...), wrapping to WETH.
  6. 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 0x6a818c673b098621e9bfb2adc80060906cf7b327 CREATEs helper 0x64c589f3ef894678e46af3b851aa08be3f40a674.
  • Path .0: helper CREATEs child 0xcc9be93051e8ad00a70eba3df2571a18f94d5856.
  • Path .0.0: child STATICCALLs yvWETH balanceOf(address) (0x70a08231) for victim 0x98289e...; output 384667252984919210375.
  • Path .0.1: child STATICCALLs yvWETH allowance(address,address) (0xdd62ed3e) for (victim, 0x143a...); output is uint256.max.
  • Path .0.2: child -> vulnerable 0x143a..., CALL, selector 0x49650044 (execute(Action[])).
    • Path .0.2.0 (action type 2 — arbitrary call): vulnerable -> yvWETH, transferFrom(address,address,uint256) (0x23b872dd) from victim to vulnerable for 384.667252984919210375 yvWETH.
    • Path .0.2.1 (action type 2 — arbitrary call): vulnerable -> yvWETH, withdraw(uint256,address,uint256) (0xe63697c8) with shares 384.667252984919210375, recipient 0x143a..., and maxLoss = 10000.
    • During the yvWETH redemption, Yearn strategies withdrew assets, including 0x85907b1a... and 0x740e59f1...; the latter exchanged stETH for ETH through Curve pool 0xdc24316b... 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.2 since 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 .0.4: child -> WETH, withdraw(uint256) (0x2e1a7d4d) for 429.210570004163139903 WETH.
  • Path .0.5: child forwards 429.210570004163139903 ETH to helper.
  • Path .1: helper forwards 429.210570004163139903 ETH 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 tx 0x52b4e5703a2d06cc6bf6842edb04961cafdf5357178813a5606e5ea9c1846fae, block 24909652.
  • Receipt log 274: yvWETH transfer from victim 0x98289e... to 0x143a..., amount 384667252984919210375.
  • Receipt log 285: yvWETH burn from 0x143a..., amount 384667252984919210375.
  • Receipt log 286: WETH transfer from yvWETH vault to 0x143a..., amount 429210570004163139885.
  • Receipt log 288: WETH transfer from 0x143a... to attacker child 0xcc9b..., amount 429210570004163139903.
  • Receipt log 290: WETH Withdrawal(address,uint256) by attacker child for 429210570004163139903 WETH.
  • Selector verification: transferFrom(address,address,uint256) = 0x23b872dd, withdraw(uint256,address,uint256) = 0xe63697c8, transfer(address,uint256) = 0xa9059cbb, withdraw(uint256) = 0x2e1a7d4d, allowance(address,address) = 0xdd62ed3e, and balanceOf(address) = 0x70a08231.
  • Exploit entry point: selector 0x49650044 resolves to execute((uint8,bytes)[]) per Dedaub decompilation of 0x143a.... The contract is a general-purpose batch action executor with no access control on execute().
  • Event ActionExecuted(uint256, uint8) (selector 0x337596d5) was emitted by the contract after each action in the batch, confirming the action type dispatch.

文章来源: https://www.darknavy.org/web3/exploits/yvweth-approval-arbitrary-command-drain/
如有侵权请联系:admin#unsafe.sh