We do not have a reliable USD price for the recorded assets yet.
0x23fcf9d4517f7cc39815b09b0a80c023ab2c8196c826c93b4100f2e26b7012860x65c210c59b43eb68112b7a4f75c8393c36491f06Ethereum0x9cbf099ff424979439dfba03f00b5961784c06ceEthereumAt Ethereum block 19325937, attacker EOA 0x94641c01a4937f2c8ef930580cf396142a2942dc called Seneca Chamber 0x65c210c59b43eb68112b7a4f75c8393c36491f06 with performOperations(uint8[],uint256[],bytes[]) and encoded a single OPERATION_CALL action. Chamber then executed PendlePrincipalToken.transferFrom(victim, attacker, amount) as Chamber itself and moved 1385238431763437306795 PT-rsETH-27JUN2024 from victim holder 0x9cbf099ff424979439dfba03f00b5961784c06ce to the attacker in one transaction.
The root cause is an unrestricted arbitrary external-call primitive in Chamber. Because Chamber accepted arbitrary non-blacklisted callees and arbitrary calldata from any caller, any third-party ERC20 approval previously granted to Chamber became a publicly spendable capability.
Seneca Chamber exposed a batching entrypoint, performOperations, that supports multiple operation types, including OPERATION_CALL = 30. In the verified Chamber source, this operation routes into _call, which decodes attacker-supplied (callee, callData, useValue1, useValue2, returnValues) and executes a raw external call from Chamber's own address after only checking require(!blacklisted[callee], "Chamber: can't call").
PendlePrincipalToken inherits . That implementation uses as the spender, calls , and then transfers tokens. As a result, when Chamber performs the external call, the token contract treats Chamber as the spender, not the EOA that invoked Chamber.
PendleERC20.transferFrommsg.sender_spendAllowance(from, spender, amount)The incident pre-state at block 19325936 included three conditions that made the drain immediately realizable: the victim holder still owned 1385238431763437306795 PT, the victim had granted Chamber type(uint256).max allowance on that PT token, and the PT token address was not blacklisted by Chamber.
This is an access-control failure in a generic call primitive, not a pricing issue or a reentrancy issue. Chamber exposed protocol identity to arbitrary callers by allowing any external user to make Chamber call arbitrary non-blacklisted contracts with arbitrary calldata. That design breaks the safety boundary that should separate protocol-owned authority from untrusted user intent.
The critical invariant is straightforward: an unrelated external caller must not be able to make Chamber spend assets or consume allowances that belong to another user unless the operation is part of an explicitly authorized Chamber workflow with user-specific checks. Chamber violated that invariant in the performOperations -> OPERATION_CALL -> _call path.
The code-level breakpoint is _call itself. After decoding attacker input, it only checks whether the callee is blacklisted and then executes callee.call{value: value}(callData). For ERC20 transferFrom, that means any approval to Chamber can be exercised by any public caller who supplies suitable calldata.
The verified Chamber source shows the dangerous path directly:
function _call(
uint256 value,
bytes memory data,
uint256 value1,
uint256 value2
) whenNotPaused internal returns (bytes memory, uint8) {
(address callee, bytes memory callData, bool useValue1, bool useValue2, uint8 returnValues) =
abi.decode(data, (address, bytes, bool, bool, uint8));
require(!blacklisted[callee], "Chamber: can't call");
(bool success, bytes memory returnData) = callee.call{value: value}(callData);
require(success, "Chamber: call failed");
return (returnData, returnValues);
}
performOperations dispatches action 30 into that function:
} else if (action == Constants.OPERATION_CALL) {
(bytes memory returnData, uint8 returnValues) = _call(values[i], datas[i], value1, value2);
The attacker exploited exactly that path. The seed transaction calldata targets performOperations(uint8[],uint256[],bytes[]) and embeds a nested transferFrom(address,address,uint256) call to PendlePrincipalToken with victim 0x9cbf099ff424979439dfba03f00b5961784c06ce, attacker 0x94641c01a4937f2c8ef930580cf396142a2942dc, and amount 1385238431763437306795.
The token-side code explains why this succeeds:
function transferFrom(
address from,
address to,
uint256 amount
) external virtual override nonReentrant returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
Because Chamber is the immediate caller, _msgSender() is Chamber. Since the victim had already approved Chamber with infinite allowance, the token accepted Chamber as the spender and transferred the victim's tokens to the attacker.
The trace and state diff confirm the mechanism and the outcome:
PendlePrincipalToken::transferFrom(
0x9CBF099ff424979439dFBa03F00B5961784c06ce,
0x94641c01a4937f2c8ef930580cf396142a2942dc,
1385238431763437306795
)
{
"holder": "0x9cbf099ff424979439dfba03f00b5961784c06ce",
"before": "1385238431763437306795",
"after": "0",
"delta": "-1385238431763437306795"
}
{
"holder": "0x94641c01a4937f2c8ef930580cf396142a2942dc",
"before": "0",
"after": "1385238431763437306795",
"delta": "1385238431763437306795"
}
This is an ACT exploit because it requires no privileged role, no attacker-side historical contract, and no private information. A fresh unprivileged caller can reproduce the behavior on public state whenever a victim has approved Chamber and the target token is not blacklisted.
The adversary flow is a single public transaction.
0x94641c01a4937f2c8ef930580cf396142a2942dc sends transaction 0x23fcf9d4517f7cc39815b09b0a80c023ab2c8196c826c93b4100f2e26b701286 to Chamber 0x65c210c59b43eb68112b7a4f75c8393c36491f06.performOperations(uint8[],uint256[],bytes[]).actions[0] is 30, which is Chamber's OPERATION_CALL.0xb05cabcd99cf9a73b19805edefc5f67ca5d1895e and the calldata to transferFrom(victimHolder, attacker, 1385238431763437306795).msg.sender.The seed metadata and trace pin the exact transaction and selector pair:
tx: 0x23fcf9d4517f7cc39815b09b0a80c023ab2c8196c826c93b4100f2e26b701286
to: 0x65c210c59b43eb68112b7a4f75c8393c36491f06
selector: 0x568d8cd9 -> performOperations(uint8[],uint256[],bytes[])
inner selector: 0x23b872dd -> transferFrom(address,address,uint256)
The measurable loss in the seed incident is the full victim PT balance:
PT-rsETH-27JUN20240xb05cabcd99cf9a73b19805edefc5f67ca5d1895e1385238431763437306795 raw units18The broader impact is larger than the single observed transfer. Any user-approved token reachable through Chamber's unrestricted call path was exposed to the same theft pattern unless specifically blacklisted or otherwise protected.
0x23fcf9d4517f7cc39815b09b0a80c023ab2c8196c826c93b4100f2e26b7012860x65c210c59b43eb68112b7a4f75c8393c36491f060xb05cabcd99cf9a73b19805edefc5f67ca5d1895e0x9cbf099ff424979439dfba03f00b5961784c06ce/workspace/session/artifacts/collector/seed/1/0x23fcf9d4517f7cc39815b09b0a80c023ab2c8196c826c93b4100f2e26b701286/metadata.json/workspace/session/artifacts/collector/seed/1/0x23fcf9d4517f7cc39815b09b0a80c023ab2c8196c826c93b4100f2e26b701286/trace.cast.log/workspace/session/artifacts/collector/seed/1/0x23fcf9d4517f7cc39815b09b0a80c023ab2c8196c826c93b4100f2e26b701286/balance_diff.json/workspace/session/artifacts/collector/seed/1/0xb05cabcd99cf9a73b19805edefc5f67ca5d1895e/src/pendle/contracts/core/erc20/PendleERC20.solhttps://etherscan.io/address/0x65c210c59b43eb68112b7a4f75c8393c36491f06#code