# CrossChainExecutor contracts do not update the necessary states for failing transactions

CrossChainExecutor reverts on failed calls without recording the attempt, so relayers can be tricked into replaying failing calls, draining gas and leaving stale calls executable indefinitely.

- Payout: Audit contest
- Severity: medium
- Program: PoolTogether
- Found by: AkshaySrivastav
- Disclosed: 2023-01-20
- Write-up quality: 6/10
- Original: https://code4rena.com/reports/2022-12-pooltogether#m-03
- Library page: http://bugbountylibrary.com/r/c4%3A2022-12-pooltogether%3AM-03
- Tags: area:web3 domain:web3 impact:dos impact:integrity kind:contest platform:code4rena sev:medium tech:evm technique:source-review vuln:business-logic vuln:dos

**Target:** CrossChainExecutor / CrossChainRelayer cross-chain call execution
**Root cause:** CallLib.executeCalls reverts the whole transaction on any failing Call, and the executor stores no state marking a call as attempted, so failed calls remain 'pending' and replayable forever.
**Steps:**
1. Attacker deploys `contract Foo { function bar() public { for(uint256 i; ; i++) {} } }` (or a reverting function).
2. Attacker relays `Foo.bar()` via CrossChainRelayer with `_gasLimit` = maxGasLimit.
3. Transport layer calls `CrossChainExecutor.executeCalls()`, which reverts, burning maxGasLimit gas.
4. No state update occurs, so the call still appears pending; transport client retries and reverts again, repeatedly draining gas.
5. For a reverting call, a second attempt reverts with `CallFailure` instead of the expected `CallsAlreadyExecuted`.
**How found:** Code review of CallLib/CrossChainExecutor: noticed CallLib reverts entirely on any failed Call and that the executor records no attempted-execution state, then reasoned about relayer retry behavior and replay of stale calls.
**Exploitation:** Attacker submits a deliberately failing call through the relayer; the transport layer client repeatedly re-executes the still-'pending' call, each attempt consuming up to maxGasLimit gas from the relayer operator. Separately, old failed calls never expire and can be broadcast later, potentially re-triggering non-idempotent actions (e.g. an unpause) at an unintended time.
**Escalation:** Lack of expiry means a stale failed call (e.g. unpause) can be executed later against a changed system state, causing non-idempotent effects.
**Insight:** When a batch executor reverts atomically on partial failure, check whether per-item execution state is persisted; off-chain relayers that retry 'pending' items can be gas-drained, and unexpired failed messages can be replayed later against changed state.

