sui_adapter_latest/
gas_charger.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5pub use checked::*;
6
7#[sui_macros::with_checked_arithmetic]
8pub mod checked {
9
10    use crate::sui_types::gas::SuiGasStatusAPI;
11    use crate::temporary_store::TemporaryStore;
12    use either::Either;
13    use indexmap::IndexMap;
14    use mysten_common::assert_reachable;
15    use sui_protocol_config::ProtocolConfig;
16    use sui_types::deny_list_v2::CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
17    use sui_types::digests::TransactionDigest;
18    use sui_types::error::ExecutionErrorTrait;
19    use sui_types::gas::{GasCostSummary, SuiGasStatus, deduct_gas};
20    use sui_types::gas_model::gas_predicates::refresh_gas_payment_location;
21    use sui_types::{
22        accumulator_event::AccumulatorEvent,
23        base_types::{ObjectID, ObjectRef, SuiAddress},
24        error::ExecutionError,
25        gas_model::tables::GasStatus,
26        is_system_package,
27        object::Data,
28    };
29    use tracing::trace;
30
31    /// Encapsulates the gas metering state (`SuiGasStatus`) and the payment source metadata,
32    /// whether it is from a smashed list (coin objects or address-balance withdrawals) or
33    /// un-metered. In other words, this serves the point of interaction between the on-chain data
34    /// (coins and address balances) and the gas meter.
35    #[derive(Debug)]
36    pub struct GasCharger {
37        tx_digest: TransactionDigest,
38        gas_model_version: u64,
39        payment: PaymentMetadata,
40        gas_status: SuiGasStatus,
41    }
42
43    /// Internal representation of how a transaction's gas is being paid.
44    /// `Unmetered` for no payment (dev inspect and system transactions).
45    /// `Gasless` for metered-but-free transactions (gas is metered but not charged).
46    /// `Smash` when one or more user-provided payment methods have been combined into a single
47    /// source.
48    #[derive(Debug)]
49    enum PaymentMetadata {
50        Unmetered,
51        Gasless,
52        /// Contains the list of payments (coins and address balances) and additional metadata
53        Smash(SmashMetadata),
54    }
55
56    /// State produced by smashing multiple gas payment sources into one.
57    /// Tracks the combined balance (`total_smashed`), the target location where the
58    /// smashed value lives, and the original payment methods for bookkeeping.
59    /// Note that the target location (`gas_charge_location`) may differ from the first payment
60    /// method in the list if it has been overridden during execution.
61    #[derive(Debug)]
62    struct SmashMetadata {
63        /// The location to charge gas from at the end of execution. Starts with the primary
64        /// payment method but may be overridden.
65        gas_charge_location: PaymentLocation,
66        /// The total balance of all smashed payment methods.
67        total_smashed: u64,
68        /// The "primary" payment method that serves as the recipient of the `total_smashed`. Also,
69        /// provides the initial location of the `gas_charge_location` before any overrides.
70        smash_target: PaymentMethod,
71        /// The original payment methods to be smashed into the `smash_target`. It does not include
72        /// the `smash_target` itself. Keyed by location to guarantee uniqueness.
73        smashed_payments: IndexMap<PaymentLocation, PaymentMethod>,
74    }
75
76    /// Public wrapper that describes how gas will be paid before smashing occurs.
77    /// Constructed via `PaymentKind::unmetered()` or `PaymentKind::smash(methods)` and
78    /// consumed by `GasCharger::new`.
79    #[derive(Debug)]
80    pub struct PaymentKind(PaymentKind_);
81
82    /// Inner representation for `PaymentKind`. Kept private so construction is forced through
83    /// the validation in `PaymentKind::smash`.
84    #[derive(Debug)]
85    enum PaymentKind_ {
86        Unmetered,
87        Gasless,
88        /// A non-empty map of gas coins or address balance withdrawals, keyed by location.
89        /// The first entry is the smash target; all others are smashed into it.
90        Smash(IndexMap<PaymentLocation, PaymentMethod>),
91    }
92
93    /// A single source of SUI used to pay for gas: either a coin object or a withdrawal
94    /// reservation from an address balance.
95    #[derive(Debug)]
96    pub enum PaymentMethod {
97        Coin(ObjectRef),
98        AddressBalance(SuiAddress, /* withdrawal reservation */ u64),
99    }
100
101    /// Identifies where a gas payment lives, independent of its value (`ObjectRef` or reservation).
102    /// Used often as a key, e.g. during smashing and during gas final charging.
103    #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
104    pub enum PaymentLocation {
105        Coin(ObjectID),
106        AddressBalance(SuiAddress),
107    }
108
109    /// A resolved gas payment: the location that will receive the final charge or refund,
110    /// paired with the total SUI available after smashing. Produced by
111    /// `GasCharger::gas_payment_amount` and consumed by PTB execution to set up the
112    /// runtime gas coin.
113    #[derive(Debug, Clone, Copy)]
114    pub struct GasPayment {
115        /// The location of the gas payment (coin or address balance), which also serves as the
116        /// target for smashed gas payments.
117        pub location: PaymentLocation,
118        /// The total amount available for gas payment after smashing
119        pub amount: u64,
120    }
121
122    impl GasCharger {
123        pub fn new(
124            tx_digest: TransactionDigest,
125            payment_kind: PaymentKind,
126            gas_status: SuiGasStatus,
127            temporary_store: &mut TemporaryStore<'_>,
128            protocol_config: &ProtocolConfig,
129        ) -> Self {
130            let gas_model_version = protocol_config.gas_model_version();
131            let payment = match payment_kind.0 {
132                PaymentKind_::Unmetered => PaymentMetadata::Unmetered,
133                PaymentKind_::Gasless => PaymentMetadata::Gasless,
134                PaymentKind_::Smash(mut payment_methods) => {
135                    let (_, smash_target) = payment_methods.shift_remove_index(0).unwrap();
136                    let mut metadata = SmashMetadata {
137                        // dummy value set below in smash_gas
138                        total_smashed: 0,
139                        gas_charge_location: smash_target.location(),
140                        smash_target,
141                        smashed_payments: payment_methods,
142                    };
143                    metadata.smash_gas(&tx_digest, temporary_store);
144                    PaymentMetadata::Smash(metadata)
145                }
146            };
147            Self {
148                tx_digest,
149                gas_model_version,
150                payment,
151                gas_status,
152            }
153        }
154
155        pub fn new_unmetered(tx_digest: TransactionDigest) -> Self {
156            Self {
157                tx_digest,
158                gas_model_version: 6, // pick any of the latest, it should not matter
159                payment: PaymentMetadata::Unmetered,
160                gas_status: SuiGasStatus::new_unmetered(),
161            }
162        }
163
164        // TODO: there is only one caller to this function that should not exist otherwise.
165        //       Explore way to remove it.
166        pub(crate) fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
167            match &self.payment {
168                PaymentMetadata::Unmetered | PaymentMetadata::Gasless => {
169                    Either::Left(std::iter::empty())
170                }
171                PaymentMetadata::Smash(metadata) => Either::Right(metadata.used_coins()),
172            }
173        }
174
175        // Override the gas payment location for smashing
176        pub fn override_gas_charge_location(
177            &mut self,
178            location: PaymentLocation,
179        ) -> Result<(), ExecutionError> {
180            if let PaymentMetadata::Smash(metadata) = &mut self.payment {
181                metadata.gas_charge_location = location;
182                Ok(())
183            } else {
184                invariant_violation!("Can only override gas charge location in the smash-gas case")
185            }
186        }
187
188        /// Return the amount available at the given input payment location.
189        /// For unmetered, this is None.
190        /// For smashed gas payments, this is the payment location and the total amount smashed.
191        /// This information feels a bit brittle but should be used only by PTB execution.
192        /// This might also differ from the final charge location, if override_gas_charge_location
193        /// is used.
194        pub fn gas_payment_amount(&self) -> Option<GasPayment> {
195            match &self.payment {
196                PaymentMetadata::Unmetered | PaymentMetadata::Gasless => None,
197                PaymentMetadata::Smash(metadata) => Some(GasPayment {
198                    location: metadata.smash_target.location(),
199                    amount: metadata.total_smashed,
200                }),
201            }
202        }
203
204        /// The coin that receives the final gas charge, or `None` when gas is paid from an address
205        /// balance (or there is no payment, e.g. unmetered/gasless).
206        pub fn gas_coin(&self) -> Option<ObjectID> {
207            self.gas_payment_amount().and_then(|gp| match gp.location {
208                PaymentLocation::Coin(coin_id) => Some(coin_id),
209                PaymentLocation::AddressBalance(_) => None,
210            })
211        }
212
213        pub(crate) fn gas_payment_location(&self) -> Option<PaymentLocation> {
214            match &self.payment {
215                PaymentMetadata::Unmetered | PaymentMetadata::Gasless => None,
216                PaymentMetadata::Smash(metadata) => Some(metadata.gas_charge_location),
217            }
218        }
219
220        pub fn gas_budget(&self) -> u64 {
221            self.gas_status.gas_budget()
222        }
223
224        pub fn unmetered_storage_rebate(&self) -> u64 {
225            self.gas_status.unmetered_storage_rebate()
226        }
227
228        pub fn no_charges(&self) -> bool {
229            self.gas_status.gas_used() == 0
230                && self.gas_status.storage_rebate() == 0
231                && self.gas_status.storage_gas_units() == 0
232        }
233
234        pub fn is_unmetered(&self) -> bool {
235            self.gas_status.is_unmetered()
236        }
237
238        pub fn move_gas_status(&self) -> &GasStatus {
239            self.gas_status.move_gas_status()
240        }
241
242        pub fn move_gas_status_mut(&mut self) -> &mut GasStatus {
243            self.gas_status.move_gas_status_mut()
244        }
245
246        pub fn into_gas_status(self) -> SuiGasStatus {
247            self.gas_status
248        }
249
250        pub fn summary(&self) -> GasCostSummary {
251            self.gas_status.summary()
252        }
253
254        // This function is called when the transaction is about to be executed.
255        // It will smash all gas coins into a single one and set the logical gas coin
256        // to be the first one in the list.
257        // After this call, `gas_coin` will return it id of the gas coin.
258        // This function panics if errors are found while operation on the gas coins.
259        // Transaction and certificate input checks must have insured that all gas coins
260        // are correct.
261        fn smash_gas(&mut self, temporary_store: &mut TemporaryStore<'_>) {
262            match &mut self.payment {
263                PaymentMetadata::Unmetered | PaymentMetadata::Gasless => (),
264                PaymentMetadata::Smash(smash_metadata) => {
265                    smash_metadata.smash_gas(&self.tx_digest, temporary_store);
266                }
267            }
268        }
269
270        //
271        // Gas charging operations
272        //
273
274        pub fn track_storage_mutation(
275            &mut self,
276            object_id: ObjectID,
277            new_size: usize,
278            storage_rebate: u64,
279        ) -> u64 {
280            self.gas_status
281                .track_storage_mutation(object_id, new_size, storage_rebate)
282        }
283
284        pub fn reset_storage_cost_and_rebate(&mut self) {
285            self.gas_status.reset_storage_cost_and_rebate();
286        }
287
288        pub fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
289            self.gas_status.charge_publish_package(size)
290        }
291
292        pub fn charge_coin_transfers(
293            &mut self,
294            protocol_config: &ProtocolConfig,
295            num_non_gas_coin_owners: u64,
296        ) -> Result<(), ExecutionError> {
297            // times two for the global pause and per-address settings
298            // this "overcharges" slightly since it does not check the global pause for each owner
299            // but rather each coin type.
300            let bytes_read_per_owner = CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
301            // associate the cost with dynamic field access so that it will increase if/when this
302            // cost increases
303            let cost_per_byte =
304                protocol_config.dynamic_field_borrow_child_object_type_cost_per_byte() as usize;
305            let cost_per_owner = bytes_read_per_owner * cost_per_byte;
306            let owner_cost = cost_per_owner * (num_non_gas_coin_owners as usize);
307            self.gas_status.charge_storage_read(owner_cost)
308        }
309
310        /// Resets any mutations, deletions, and events recorded in the store, as well as any storage costs and
311        /// rebates, then Re-runs gas smashing. Effects on store are now as if we were about to begin execution
312        pub fn reset(&mut self, temporary_store: &mut TemporaryStore<'_>) {
313            temporary_store.drop_writes();
314            self.gas_status.reset_storage_cost_and_rebate();
315            self.smash_gas(temporary_store);
316        }
317
318        // === legacy methods below — see `mod legacy` for the full bodies ===
319    }
320
321    /// Frozen pre-v15 (`gas_model_version < 15`) gas charging, mirroring `origin/main` (incl. the
322    /// SUIPR-753 fix). Removed at the next execution-version cut.
323    mod legacy {
324        use super::*;
325
326        impl super::GasCharger {
327            /// Pre-v15 input charging: sum all input sizes and charge
328            /// `storage_read` once. Bit-for-bit identical to origin/main for
329            /// replay determinism; goes away when execution_version > 4.
330            pub fn charge_input_objects_legacy(
331                &mut self,
332                temporary_store: &TemporaryStore<'_>,
333            ) -> Result<(), ExecutionError> {
334                let objects = temporary_store.objects();
335                // TODO: Charge input object count.
336                let _object_count = objects.len();
337                // Charge bytes read
338                let total_size = temporary_store
339                    .objects()
340                    .iter()
341                    // don't charge for loading Sui Framework or Move stdlib
342                    .filter(|(id, _)| !is_system_package(**id))
343                    .map(|(_, obj)| obj.object_size_for_gas_metering())
344                    .sum();
345                self.gas_status.charge_storage_read(total_size)
346            }
347
348            /// Entry point for legacy gas charging.
349            /// 1. Compute tx storage gas costs and tx storage rebates, update storage_rebate field
350            /// of mutated objects
351            /// 2. Deduct computation gas costs and storage costs, credit storage rebates.
352            /// The happy path of this function follows (1) + (2) and is fairly simple.
353            /// Most of the complexity is in the unhappy paths:
354            /// - if execution aborted before calling this function, we have to dump all writes +
355            ///   re-smash gas, then charge for storage
356            /// - if we run out of gas while charging for storage, we have to dump all writes +
357            ///   re-smash gas, then charge for storage again
358            pub(crate) fn legacy_charge_gas<T, E: ExecutionErrorTrait>(
359                &mut self,
360                temporary_store: &mut TemporaryStore<'_>,
361                protocol_config: &ProtocolConfig,
362                execution_result: &mut Result<T, E>,
363            ) -> GasCostSummary {
364                // at this point, we have done *all* charging for computation,
365                // but have not yet set the storage rebate or storage gas units
366                debug_assert!(self.gas_status.storage_rebate() == 0);
367                debug_assert!(self.gas_status.storage_gas_units() == 0);
368
369                if !matches!(&self.payment, PaymentMetadata::Unmetered) {
370                    // bucketize computation cost
371                    let is_move_abort = execution_result
372                        .as_ref()
373                        .err()
374                        .map(|err| {
375                            matches!(
376                                err.kind(),
377                                sui_types::execution_status::ExecutionErrorKind::MoveAbort(_, _)
378                            )
379                        })
380                        .unwrap_or(false);
381                    // bucketize computation cost
382                    if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
383                        && execution_result.is_ok()
384                    {
385                        *execution_result = Err(err.into());
386                    }
387
388                    // On error we need to dump writes, deletes, etc before charging storage gas
389                    if execution_result.is_err() {
390                        self.reset(temporary_store);
391                    }
392                }
393
394                // compute and collect storage charges
395                temporary_store.ensure_active_inputs_mutated();
396                temporary_store.collect_storage_and_rebate(self);
397
398                if matches!(&self.payment, PaymentMetadata::Unmetered) {
399                    return GasCostSummary::default();
400                }
401                let gas_payment_location = self.gas_payment_location();
402                if let Some(PaymentLocation::Coin(_)) = gas_payment_location {
403                    #[skip_checked_arithmetic]
404                    trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
405                }
406
407                if execution_result
408                .as_ref()
409                .err()
410                .map(|err| {
411                    matches!(
412                        err.kind(),
413                        sui_types::execution_status::ExecutionErrorKind::InsufficientFundsForWithdraw
414                    )
415                })
416                .unwrap_or(false)
417                && matches!(gas_payment_location, Some(PaymentLocation::AddressBalance(_))) {
418                    debug_assert!(!protocol_config.early_exit_on_iffw(), "Should have not reached charge gas in this case with IFFW");
419                    // If we don't have enough balance to withdraw, don't charge for gas
420                    // TODO: consider charging gas if we have enough to reserve but not enough to cover all withdraws
421                    return GasCostSummary::default();
422            }
423
424                self.compute_storage_and_rebate(temporary_store, execution_result);
425
426                let gas_payment_location = if refresh_gas_payment_location(self.gas_model_version) {
427                    self.gas_payment_location()
428                } else {
429                    gas_payment_location
430                };
431
432                let cost_summary = self.gas_status.summary();
433
434                let Some(gas_payment_location) = gas_payment_location else {
435                    // Gasless: sender pays nothing.
436                    assert!(
437                        matches!(self.payment, PaymentMetadata::Gasless),
438                        "Only gasless transactions should reach this point without a payment location"
439                    );
440                    if execution_result.is_err() {
441                        return GasCostSummary::default();
442                    }
443                    // Any storage rebate from destroyed input coins is absorbed as
444                    // network fees, not returned to sender.
445                    let storage_cost = cost_summary.storage_cost;
446                    assert!(
447                        storage_cost == 0,
448                        "Gasless transaction must not incur storage cost, got {storage_cost}"
449                    );
450                    let sender_rebate = cost_summary.storage_rebate;
451                    return GasCostSummary {
452                        computation_cost: sender_rebate,
453                        storage_cost: 0,
454                        storage_rebate: sender_rebate,
455                        non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
456                    };
457                };
458
459                let net_change = cost_summary.net_gas_usage();
460
461                match gas_payment_location {
462                    PaymentLocation::AddressBalance(payer_address) => {
463                        // TODO tracing?
464                        if net_change != 0 {
465                            let balance_type = sui_types::balance::Balance::type_tag(
466                                sui_types::gas_coin::GAS::type_tag(),
467                            );
468                            let event = AccumulatorEvent::from_balance_change(
469                                payer_address,
470                                balance_type,
471                                net_change.checked_neg().unwrap(),
472                            )
473                            .expect("Failed to create accumulator event for gas charging");
474                            temporary_store.add_accumulator_event(event);
475                        }
476                    }
477                    PaymentLocation::Coin(gas_object_id) => {
478                        let mut gas_object =
479                            temporary_store.read_object(&gas_object_id).unwrap().clone();
480                        deduct_gas(&mut gas_object, net_change);
481                        #[skip_checked_arithmetic]
482                        trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
483                        temporary_store.mutate_new_or_input_object(gas_object);
484                    }
485                }
486                cost_summary
487            }
488
489            /// Calculate total gas cost considering storage and rebate.
490            ///
491            /// First, we net computation, storage, and rebate to determine total gas to charge.
492            ///
493            /// If we exceed gas_budget, we set execution_result to InsufficientGas, failing the tx.
494            /// If we have InsufficientGas, we determine how much gas to charge for the failed tx:
495            ///
496            /// v1: we set computation_cost = gas_budget, so we charge net (gas_budget - storage_rebates)
497            /// v2: we charge (computation + storage costs for input objects - storage_rebates)
498            ///     if the gas balance is still insufficient, we fall back to set computation_cost = gas_budget
499            ///     so we charge net (gas_budget - storage_rebates)
500            fn compute_storage_and_rebate<T, E: ExecutionErrorTrait>(
501                &mut self,
502                temporary_store: &mut TemporaryStore<'_>,
503                execution_result: &mut Result<T, E>,
504            ) {
505                if let Err(err) = self.gas_status.charge_storage_and_rebate() {
506                    // we run out of gas charging storage, reset and try charging for storage again.
507                    // Input objects are touched and so they have a storage cost
508                    // Attempt to charge just for computation + input object storage costs - storage_rebate
509                    self.reset(temporary_store);
510                    temporary_store.ensure_active_inputs_mutated();
511                    temporary_store.collect_storage_and_rebate(self);
512                    if let Err(err) = self.gas_status.charge_storage_and_rebate() {
513                        // we run out of gas attempting to charge for the input objects exclusively,
514                        // deal with this edge case by not charging for storage: we charge (gas_budget - rebates).
515                        self.reset(temporary_store);
516                        self.gas_status.adjust_computation_on_out_of_gas();
517                        temporary_store.ensure_active_inputs_mutated();
518                        temporary_store.collect_rebate(self);
519                        if execution_result.is_ok() {
520                            *execution_result = Err(err.into());
521                        }
522                    } else if execution_result.is_ok() {
523                        *execution_result = Err(err.into());
524                    }
525                }
526            }
527        }
528    }
529
530    impl SmashMetadata {
531        /// Iterates over all payment methods: the smash target followed by the smashed payments.
532        fn payment_methods(&self) -> impl Iterator<Item = &'_ PaymentMethod> {
533            std::iter::once(&self.smash_target).chain(self.smashed_payments.values())
534        }
535
536        fn smash_gas(
537            &mut self,
538            tx_digest: &TransactionDigest,
539            temporary_store: &mut TemporaryStore<'_>,
540        ) {
541            // set gas charge location
542            self.gas_charge_location = self.smash_target.location();
543
544            // sum the value of all gas coins
545            let total_smashed = self
546                .payment_methods()
547                .map(|payment| match payment {
548                    PaymentMethod::AddressBalance(_, reservation) => Ok(*reservation),
549                    PaymentMethod::Coin(obj_ref) => {
550                        let obj_data = temporary_store
551                            .objects()
552                            .get(&obj_ref.0)
553                            .map(|obj| &obj.data);
554                        let Some(Data::Move(move_obj)) = obj_data else {
555                            return Err(ExecutionError::invariant_violation(
556                                "Provided non-gas coin object as input for gas!",
557                            ));
558                        };
559                        if !move_obj.type_().is_gas_coin() {
560                            return Err(ExecutionError::invariant_violation(
561                                "Provided non-gas coin object as input for gas!",
562                            ));
563                        }
564                        Ok(move_obj.get_coin_value_unsafe())
565                    }
566                })
567                .collect::<Result<Vec<u64>, ExecutionError>>()
568                // transaction and certificate input checks must have insured that all gas coins
569                // are valid
570                .unwrap_or_else(|_| {
571                    panic!(
572                        "Unable to process gas payments for transaction {}",
573                        tx_digest
574                    )
575                })
576                .iter()
577                .sum();
578            // If it is 0, then we are smashing for the first time (at the beginning of execution).
579            // If it is non-zero, then we are re-smashing after a reset (due to some sort of
580            // failure in charging for gas), and the total should not change.
581            debug_assert!(
582                self.total_smashed == 0 || self.total_smashed == total_smashed,
583                "Gas smashing should not change after a reset"
584            );
585            self.total_smashed = total_smashed;
586
587            let smash_location = self.smash_target.location();
588            // delete all gas objects except the smash target
589            for payment_method in self.smashed_payments.values() {
590                let location = payment_method.location();
591                assert_ne!(location, smash_location, "Payment methods must be unique");
592                match payment_method {
593                    PaymentMethod::AddressBalance(sui_address, reservation) => {
594                        assert_reachable!("smashed payment is address-balance reservation");
595                        let balance_type = sui_types::balance::Balance::type_tag(
596                            sui_types::gas_coin::GAS::type_tag(),
597                        );
598                        let event = AccumulatorEvent::from_balance_change(
599                            *sui_address,
600                            balance_type,
601                            i64::try_from(*reservation).unwrap().checked_neg().unwrap(),
602                        )
603                        .expect("Failed to create accumulator event for gas smashing");
604                        temporary_store.add_accumulator_event(event);
605                    }
606                    PaymentMethod::Coin((id, _, _)) => {
607                        assert_reachable!("smashed payment is coin object");
608                        temporary_store.delete_input_object(id);
609                    }
610                }
611            }
612            match &self.smash_target {
613                PaymentMethod::AddressBalance(sui_address, reservation) => {
614                    assert_reachable!("smash target is address-balance reservation");
615                    // The reservation here is only a maximal withdrawal from this address balance
616                    // We do not need to withdraw here unless necessary, which will be done during
617                    // gas charging
618                    let deposit = total_smashed - *reservation;
619                    if deposit != 0 {
620                        let balance_type = sui_types::balance::Balance::type_tag(
621                            sui_types::gas_coin::GAS::type_tag(),
622                        );
623                        let event = AccumulatorEvent::from_balance_change(
624                            *sui_address,
625                            balance_type,
626                            i64::try_from(deposit).unwrap(),
627                        )
628                        .expect("Failed to create accumulator event for gas smashing");
629                        temporary_store.add_accumulator_event(event);
630                    }
631                }
632                PaymentMethod::Coin((gas_coin_id, _, _)) => {
633                    let mut primary_gas_object = temporary_store
634                        .objects()
635                        .get(gas_coin_id)
636                        // unwrap should be safe because we checked that this exists in `self.objects()` above
637                        .unwrap_or_else(|| {
638                            panic!(
639                                "Invariant violation: gas coin not found in store in txn {}",
640                                tx_digest
641                            )
642                        })
643                        .clone();
644                    primary_gas_object
645                        .data
646                        .try_as_move_mut()
647                        // unwrap should be safe because we checked that the primary gas object was a coin object above.
648                        .unwrap_or_else(|| {
649                            panic!(
650                                "Invariant violation: invalid coin object in txn {}",
651                                tx_digest
652                            )
653                        })
654                        .set_coin_value_unsafe(total_smashed);
655                    temporary_store.mutate_input_object(primary_gas_object);
656                }
657            }
658        }
659
660        fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
661            self.payment_methods().filter_map(|method| match method {
662                PaymentMethod::Coin(obj_ref) => Some(obj_ref),
663                PaymentMethod::AddressBalance(_, _) => None,
664            })
665        }
666    }
667
668    impl PaymentKind {
669        pub fn unmetered() -> Self {
670            Self(PaymentKind_::Unmetered)
671        }
672
673        pub fn gasless() -> Self {
674            Self(PaymentKind_::Gasless)
675        }
676
677        pub fn smash(payment_methods: Vec<PaymentMethod>) -> Option<Self> {
678            debug_assert!(
679                !payment_methods.is_empty(),
680                "GasCharger must have at least one payment method"
681            );
682            if payment_methods.is_empty() {
683                return None;
684            }
685            let mut unique_methods = IndexMap::new();
686            for payment_method in payment_methods {
687                match (
688                    unique_methods.entry(payment_method.location()),
689                    payment_method,
690                ) {
691                    (indexmap::map::Entry::Vacant(entry), payment_method) => {
692                        entry.insert(payment_method);
693                    }
694                    (
695                        indexmap::map::Entry::Occupied(mut occupied),
696                        PaymentMethod::AddressBalance(other, additional),
697                    ) => {
698                        let PaymentMethod::AddressBalance(addr, amount) = occupied.get_mut() else {
699                            unreachable!("Payment method does not match location")
700                        };
701                        assert_eq!(*addr, other, "Payment method does not match location");
702                        *amount += additional;
703                    }
704                    (indexmap::map::Entry::Occupied(_), _) => {
705                        debug_assert!(
706                            false,
707                            "Duplicate coin payment method found, \
708                             which should have been prevented by input checks"
709                        );
710                        return None;
711                    }
712                }
713            }
714            Some(Self(PaymentKind_::Smash(unique_methods)))
715        }
716    }
717
718    impl PaymentMethod {
719        pub fn location(&self) -> PaymentLocation {
720            match self {
721                PaymentMethod::Coin(obj_ref) => PaymentLocation::Coin(obj_ref.0),
722                PaymentMethod::AddressBalance(addr, _) => PaymentLocation::AddressBalance(*addr),
723            }
724        }
725    }
726}