pallet-transaction-payment has ChargeTransactionPayment that implements SignedExtension. In its code, both validate() and pre_dispatch() call withdraw_fee() internally.
Why doesn't this cause multiple withdrawals?
Also, validate() could be called many times.
https://github.com/paritytech/substrate/blob/v2.0.0/frame/transaction-payment/src/lib.rs#L511-L534
During validation phase, all state changes are discarded. Validation phase in used while the transaction lives in the pool. While being authored and imported, it only goes through
pre_dispatch.Indeed, the pool (or generally any component outside of the runtime) might want to re-validate the transaction multiple times. Key is to remember that the state changes are discarded in validation.
See more here: https://github.com/paritytech/substrate/blob/a200cdb93c6af5763b9c7bf313fa708764ac88ca/primitives/runtime/src/traits.rs#L711-L728