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