Arbitrary calldata execution in xReserve via forwarding hook leads to token drain from reserve/gateway wallet

Summary:

A forwarding hook in xReserve allows arbitrary calldata to be executed against TokenMessenger / TokenMessengerV2 when processing a withdrawal attestation. Because xReserve sets unlimited allowances for TokenMessenger (so it can operate), an attacker who can supply a valid attestation (or if an attestation signer is compromised / a malicious signer is added) can craft forwarding calldata that triggers a token pull (e.g., depositForBurn/drain pattern) and cause tokens to be transferred out of the reserve/gateway wallet.

Steps To Reproduce:

  1. Prepare environment: deploy or fork a chain with xReserve, GatewayWallet, and TokenMessenger instances; ensure xReserve supports a token and sets unlimited allowances for TokenMessenger (this is the default behavior in TokenSupport).
  2. Seed funds: deposit tokens into the reserve/gateway wallet so there are funds available for withdrawal.
  3. Craft malicious forwarding calldata that calls a TokenMessenger function that performs transferFrom(msg.sender, to, amount) (e.g., depositForBurn or a mock drain function). Example calldata: abi.encodeWithSelector(TokenMessenger.depositForBurn.selector, amount, destDomain, recipientBytes32, tokenAddress).
  4. Build an attestation (or use the PoC attestation signer in the test harness) whose TransferSpec contains hook data with forwardingContract pointing to TokenMessenger and forwardingCalldata set to the payload from step 3. Also set destinationCaller so the reserve is a valid caller per validation (the PoC tests handle this).
  5. Call reserve.withdraw(encodedAttestations, signature) from an attacker-controlled account supplying the crafted attestation and signature.
  6. Observe: xReserve executes the forwarding via Address.functionCall(forwardingContract, forwardingCalldata) and TokenMessenger will transferFrom from the reserve/local minter, moving tokens to the attacker/recipient.

Supporting Material/References:

Attestation is not easily forged — signature verification is via ECDSA and must come from an address in the attestationSigners set (lib/…/Mints.sol::_verifyAttestationSignature). However, the exploit remains real when one of the conditions/prerequisites is met:

  • Compromise of one of the attestation signers' private keys (key leak, phish, or HSM/multisig compromise),
  • Owner/gov adds malicious or misconfigured signers (e.g. owner adds signers without review),
  • Replay/misconfiguration in the attestation/allowlist process allows attackers to issue valid attestations.

Relevant source locations:

  • src/modules/x-reserve/Withdrawal.sol_validateAndProcessHookData / _processForwarding (executes Address.functionCall on the forwarded calldata without selector whitelist)
  • src/modules/x-reserve/TokenSupport.sol_setUnlimitedAllowances (xReserve gives unlimited allowance to tokenMessenger / tokenMessengerV2)
  • lib/evm-cctp-contracts/src/TokenMessenger.sol and lib/evm-cctp-contracts/src/v2/BaseTokenMessenger.sol_depositForBurn / _depositAndBurn invoke transferFrom(msg.sender, ...) (the token pull occurs here)
  • lib/evm-gateway-contracts/src/modules/minter/Mints.sol_verifyAttestationSignature and isAttestationSigner (attestation signature verification and signer allowlist)

A possible question is, what if the Wallet gateway is empty? That means the attacker won't gain any profit. just need to monitor or the "attacker" can create their own balance,

Flash loan amplification Attacker can borrow → deposit → drain → repay .

The point is that what you have to outwit is the SIGNER PART

buyme coffee: 0x136e744b1a2a2816b98556de69c636dd35e65306