# Mt Pelerin Double Transaction Bugfix Review

cancelOnHoldTransfers in Mt Pelerin's bridge-protocol-v2 lacked a check for already-canceled transfers, letting an attacker pass duplicate transaction IDs and drain contract funds.

- Payout: $10,000
- Severity: critical
- Program: Mt Pelerin
- Disclosed: 2022-11-23
- Write-up quality: 6/10
- Original: https://immunefi.com/blog/bug-fix-reviews/mt-pelerin-double-transaction-bugfix-review/
- Library page: https://bugbountylibrary.com/r/blog%3Aimmunefi-com-blog-bug-fix-reviews-mt-pelerin-double-transaction-bugfix-review
- Tags: domain:web3 impact:funds-loss kind:bounty platform:blog platform:immunefi sev:critical tech:evm tech:solidity technique:source-review vuln:business-logic

**Target:** Solidity smart contract (ComplianceRegistry.sol) cancelOnHoldTransfers function
**Root cause:** The cancelOnHoldTransfers loop sets decision=TRANSFER_CANCEL and refunds the token but never verifies the transfer's prior status, so the same on-hold transfer can be canceled repeatedly.
**Steps:**
1. Call `cancelOnHoldTransfers(trustedIntermediary, transfers, skipMinBoundaryUpdate)` with `transfers` being an array containing the same on-hold transfer ID repeated N times.
2. Each iteration passes `require(transfer.from == _msgSender())` since the transfer belongs to the attacker.
3. Contract sets `onHoldTransfers[trustedIntermediary][id].decision = TRANSFER_CANCEL` and calls `IERC20Detailed(transfer.token).transfer(transfer.from, transfer.amount)` each iteration.
4. No check that decision was already TRANSFER_CANCEL, so the refund is paid N times, draining the contract.
**How found:** Whitehat reviewed the cancelOnHoldTransfers loop and noticed it mutated state and paid out per array element without checking whether the transfer had already been canceled, i.e. missing idempotency check on user-supplied array.
**Exploitation:** Attacker first has an on-hold transfer of their own (non-compliant transfer flagged by the processor). They then call cancelOnHoldTransfers with an array of that same transfer ID repeated many times, receiving the transfer amount refunded once per array element until the contract's token balance is exhausted.
**Insight:** In batch-cancel/refund loops over user-supplied ID arrays, check that each element's state transition is valid (e.g. status still on-hold) before paying out; duplicate IDs in the array are the classic drain vector.
