JamSettlement Allowance Sweep
Exploit Transactions
0xe5f8fe69b38613a855dbcb499a2c4ecffe318c620a4c4117bd0e298213b7619dVictim Addresses
0xbeb0b0623f66be8ce162ebdfa2ec543a522f4ea6ArbitrumLoss Breakdown
Similar Incidents
DEI burnFrom Allowance Inversion
37%Balancer Callback Drain
29%The Standard Self-Swap Bad Debt
28%Dexible selfSwap allowance drain
28%Paribus Redeem Reentrancy
27%Rodeo unshETH Oracle Exploit
27%Root Cause Analysis
JamSettlement Allowance Sweep
1. Incident Overview TL;DR
On Arbitrum block 367586045, transaction 0xe5f8fe69b38613a855dbcb499a2c4ecffe318c620a4c4117bd0e298213b7619d let an unprivileged adversary sweep 20,069.560783 USDC from two third-party wallets through JamSettlement. The attacker did not need a victim signature, Permit2 witness, private key compromise, or any privileged role. The root cause is that JamSettlement.settle exposes attacker-controlled arbitrary interactions while the only interaction guard blocks calls to JamBalanceManager, not direct ERC20 transferFrom calls from the settlement contract itself.
2. Key Background
JamSettlement is intended to execute user orders and optionally interact with external protocols during settlement. Jam's own balance-manager design documents the intended trust boundary: user approvals should be isolated so arbitrary settlement interactions cannot drain them. The relevant contracts are:
JamSettlementat0xbeb0b0623f66be8ce162ebdfa2ec543a522f4ea6JamBalanceManagerat0xc5a350853e4e36b73eb0c24aaa4b8816c9a3579a- Arbitrum USDC at
0xaf88d065e77c8cc2239327c5edb3a432268e5831
The verified JamBalanceManager source states the intended safety goal:
/// @notice The reason a balance manager exists is to prevent interaction to the settlement contract draining user funds
/// By having another contract that allowances are made to, we can enforce that it is only used to draw in user balances to settlement and not sent out
contract JamBalanceManager is IJamBalanceManager {
But in production, the victims had granted USDC allowance directly to JamSettlement, and USDC uses normal transferFrom spender semantics. That means any execution path that lets JamSettlement call USDC can spend those approvals.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK-category ACT issue caused by broken capability confinement inside the settlement path. The critical problem is not in USDC and not in JamBalanceManager; it is the combination of permissive order validation and unrestricted external calls from JamSettlement. In JamValidation.validateOrder, a non-Permit2 order does not require a signature when order.taker == msg.sender. In JamInteraction.runInteractions, the library only rejects calls whose target equals the balance-manager address and otherwise performs a raw external call. As a result, an attacker can submit an empty order, avoid any sell-side token transfer, and still force JamSettlement to call USDC transferFrom(victim, attacker, amount) against pre-existing victim approvals. The post-interaction buy-side checks are vacuous when the attacker uses empty buy arrays, so nothing in the flow binds the arbitrary interactions to legitimate order semantics.
4. Detailed Root Cause Analysis
The verified Jam source exposes the two key breakpoints. First, order validation:
function validateOrder(JamOrder calldata order, bytes calldata signature, bytes32 hooksHash) internal {
// Allow settle from user without sig; For permit2 case, we already validated witness during the transfer
if (order.taker != msg.sender && !order.usingPermit2) {
...
validateSignature(order.taker, orderHash, signature);
}
...
}
Second, arbitrary interaction execution:
function runInteractions(Data[] calldata interactions, IJamBalanceManager balanceManager) internal returns (bool) {
for (uint i; i < interactions.length; ++i) {
Data calldata interaction = interactions[i];
require(interaction.to != address(balanceManager), CallToBalanceManagerNotAllowed());
(bool execResult,) = payable(interaction.to).call{ value: interaction.value }(interaction.data);
if (!execResult && interaction.result) return false;
}
return true;
}
The seed trace shows the exact exploit path. The attacker helper 0x091101b0f31833c03dddd5b6411e62a212d05875 called JamSettlement::settle with empty sell and buy arrays and two interaction payloads targeting USDC transferFrom:
0xbeb0b0623f66bE8cE162EbDfA2ec543A522F4ea6::settle(
(..., [], [], [], [], false),
0x,
[
(false, USDC, 0, transferFrom(victim_one, attacker, 20068560783)),
(false, USDC, 0, transferFrom(victim_two, attacker, 1000000))
],
0x,
0x091101B0f31833C03DddD5b6411E62a212D05875
)
├─ JamBalanceManager::transferTokens([], [], ...)
├─ USDC::transferFrom(victim_one, attacker, 20068560783)
└─ USDC::transferFrom(victim_two, attacker, 1000000)
This satisfies the ACT predicate deterministically. The attacker is unprivileged, the victims' approvals already existed on-chain, and the settlement path itself becomes the spender. The relevant invariant is: a public settlement entrypoint must only consume approvals in service of the validated order's sell-side transfers, not let arbitrary callers transform protocol approvals into unrelated token transfers.
5. Adversary Flow Analysis
The adversary EOA was 0x59537353248d0b12c7fcca56a4e420ffec4abc91. Within the exploit transaction it created two short-lived contracts, 0x267acd62e4bc7c2edbb73f9698050e99833c64f6 and 0x091101b0f31833c03dddd5b6411e62a212d05875, to package the malicious call.
The execution sequence was:
- The helper contract checked victim balances and allowances to
JamSettlement. - It submitted a non-Permit2 order whose
takerequaled the helper contract itself, withexecutor = address(0),nonce = 1, live expiry, and empty sell and buy arrays. JamBalanceManager::transferTokens([], [], ...)became a no-op because the order had no sell tokens.JamInteraction.runInteractionsexecuted two attacker-chosen USDC calls, each invokingtransferFromfrom a victim to the attacker EOA.- Settlement completed because no buy-side outputs were required.
The concrete victims were 0x0c06e0737e81666023ba2a4a10693e93277cbbf1 and 0xe7ee27d53578704825cddd578cd1f15ea93eb6fd. The approvals consumed were exactly 20,068,560,783 and 1,000,000 raw USDC units.
6. Impact & Losses
The balance diff for the seed transaction records:
{
"holder": "0x0c06e0737e81666023ba2a4a10693e93277cbbf1",
"delta": "-20068560783"
}
{
"holder": "0xe7ee27d53578704825cddd578cd1f15ea93eb6fd",
"delta": "-1000000"
}
{
"holder": "0x59537353248d0b12c7fcca56a4e420ffec4abc91",
"delta": "20069560783"
}
The total measurable loss was 20,069.560783 USDC, encoded on-chain as 20069560783 raw units with 6 decimals. The exploit is permissionless and repeatable against any wallet that leaves compatible ERC20 approvals outstanding to JamSettlement.
7. References
- Seed exploit tx:
0xe5f8fe69b38613a855dbcb499a2c4ecffe318c620a4c4117bd0e298213b7619d - Seed trace:
/workspace/session/artifacts/collector/seed/42161/0xe5f8fe69b38613a855dbcb499a2c4ecffe318c620a4c4117bd0e298213b7619d/trace.cast.log - Seed balance diff:
/workspace/session/artifacts/collector/seed/42161/0xe5f8fe69b38613a855dbcb499a2c4ecffe318c620a4c4117bd0e298213b7619d/balance_diff.json - Seed metadata:
/workspace/session/artifacts/collector/seed/42161/0xe5f8fe69b38613a855dbcb499a2c4ecffe318c620a4c4117bd0e298213b7619d/metadata.json - Verified JamSettlement source:
https://api.etherscan.io/v2/api?chainid=42161&module=contract&action=getsourcecode&address=0xbeb0b0623f66be8ce162ebdfa2ec543a522f4ea6 - Verified JamBalanceManager source:
https://api.etherscan.io/v2/api?chainid=42161&module=contract&action=getsourcecode&address=0xc5a350853e4e36b73eb0c24aaa4b8816c9a3579a