# Seneca Chamber: arbitrary call in performOperations drains Pendle tokens

Seneca Chamber's performOperations exposed an arbitrary external call (op 30), letting an attacker transferFrom a victim's Pendle principal tokens worth ~$6M.

- Payout: Real-world hack
- Severity: critical
- Program: Seneca
- Analysis by: DeFiHackLabs
- Disclosed: 2024-02-28
- Write-up quality: 6/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2024-02/Seneca_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20240228-seneca
- Tags: 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:** Chamber contract performOperations (DeFi vault operation dispatcher)
**Root cause:** performOperations accepts a caller-supplied action array including OPERATION_CALL (30) and executes arbitrary target+calldata without validating the target or restricting which tokens/allowances the Chamber can move.
**Steps:**
1. Fork mainnet at block 19325936.
2. Read victim's Pendle principal token balance: `PendlePrincipalToken.balanceOf(victim)`.
3. Build calldata `transferFrom(victim, attacker, amount)` on the Pendle token.
4. ABI-encode target=Pendle token, callData, and zero padding into a single `data` blob.
5. Call `Chamber.performOperations([30],[0],[data])` with action OPERATION_CALL.
6. Chamber executes the transferFrom using its own allowance/position, moving victim tokens to attacker.
**Exploitation:** Attacker invoked the Chamber's performOperations with action 30 (CALL) pointing at the Pendle principal token, using transferFrom to pull the victim's full balance to the attacker contract in a single transaction; no capital or flash loan needed since the Chamber held the necessary approval/position.
**Insight:** When a vault exposes a generic operation dispatcher (action codes like CALL/TRANSFER), check whether the CALL opcode lets you pick an arbitrary target and calldata — any token the vault has approved or holds becomes drainable via transferFrom.
