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            execution_result: &mut Result<T, E>,
350        ) -> GasCostSummary {
351            // at this point, we have done *all* charging for computation,
352            // but have not yet set the storage rebate or storage gas units
353            debug_assert!(self.gas_status.storage_rebate() == 0);
354            debug_assert!(self.gas_status.storage_gas_units() == 0);
355
356            if !matches!(&self.payment, PaymentMetadata::Unmetered) {
357                // bucketize computation cost
358                let is_move_abort = execution_result
359                    .as_ref()
360                    .err()
361                    .map(|err| {
362                        matches!(
363                            err.kind(),
364                            sui_types::execution_status::ExecutionErrorKind::MoveAbort(_, _)
365                        )
366                    })
367                    .unwrap_or(false);
368                // bucketize computation cost
369                if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
370                    && execution_result.is_ok()
371                {
372                    *execution_result = Err(err.into());
373                }
374
375                // On error we need to dump writes, deletes, etc before charging storage gas
376                if execution_result.is_err() {
377                    self.reset(temporary_store);
378                }
379            }
380
381            // compute and collect storage charges
382            temporary_store.ensure_active_inputs_mutated();
383            temporary_store.collect_storage_and_rebate(self);
384
385            if matches!(&self.payment, PaymentMetadata::Unmetered) {
386                return GasCostSummary::default();
387            }
388            let gas_payment_location = self.gas_payment_location();
389            if let Some(PaymentLocation::Coin(_)) = gas_payment_location {
390                #[skip_checked_arithmetic]
391                trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
392            }
393
394            if execution_result
395                .as_ref()
396                .err()
397                .map(|err| {
398                    matches!(
399                        err.kind(),
400                        sui_types::execution_status::ExecutionErrorKind::InsufficientFundsForWithdraw
401                    )
402                })
403                .unwrap_or(false)
404                && matches!(gas_payment_location, Some(PaymentLocation::AddressBalance(_))) {
405                    // If we don't have enough balance to withdraw, don't charge for gas
406                    // TODO: consider charging gas if we have enough to reserve but not enough to cover all withdraws
407                    return GasCostSummary::default();
408            }
409
410            self.compute_storage_and_rebate(temporary_store, execution_result);
411
412            let gas_payment_location = if refresh_gas_payment_location(self.gas_model_version) {
413                self.gas_payment_location()
414            } else {
415                gas_payment_location
416            };
417
418            let cost_summary = self.gas_status.summary();
419
420            let Some(gas_payment_location) = gas_payment_location else {
421                // Gasless: sender pays nothing.
422                assert!(
423                    matches!(self.payment, PaymentMetadata::Gasless),
424                    "Only gasless transactions should reach this point without a payment location"
425                );
426                if execution_result.is_err() {
427                    return GasCostSummary::default();
428                }
429                // Any storage rebate from destroyed input coins is absorbed as
430                // network fees, not returned to sender.
431                let storage_cost = cost_summary.storage_cost;
432                assert!(
433                    storage_cost == 0,
434                    "Gasless transaction must not incur storage cost, got {storage_cost}"
435                );
436                let sender_rebate = cost_summary.storage_rebate;
437                return GasCostSummary {
438                    computation_cost: sender_rebate,
439                    storage_cost: 0,
440                    storage_rebate: sender_rebate,
441                    non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
442                };
443            };
444
445            let net_change = cost_summary.net_gas_usage();
446
447            match gas_payment_location {
448                PaymentLocation::AddressBalance(payer_address) => {
449                    // TODO tracing?
450                    if net_change != 0 {
451                        let balance_type = sui_types::balance::Balance::type_tag(
452                            sui_types::gas_coin::GAS::type_tag(),
453                        );
454                        let event = AccumulatorEvent::from_balance_change(
455                            payer_address,
456                            balance_type,
457                            net_change.checked_neg().unwrap(),
458                        )
459                        .expect("Failed to create accumulator event for gas charging");
460                        temporary_store.add_accumulator_event(event);
461                    }
462                }
463                PaymentLocation::Coin(gas_object_id) => {
464                    let mut gas_object =
465                        temporary_store.read_object(&gas_object_id).unwrap().clone();
466                    deduct_gas(&mut gas_object, net_change);
467                    #[skip_checked_arithmetic]
468                    trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
469                    temporary_store.mutate_new_or_input_object(gas_object);
470                }
471            }
472            cost_summary
473        }
474
475        /// Calculate total gas cost considering storage and rebate.
476        ///
477        /// First, we net computation, storage, and rebate to determine total gas to charge.
478        ///
479        /// If we exceed gas_budget, we set execution_result to InsufficientGas, failing the tx.
480        /// If we have InsufficientGas, we determine how much gas to charge for the failed tx:
481        ///
482        /// v1: we set computation_cost = gas_budget, so we charge net (gas_budget - storage_rebates)
483        /// v2: we charge (computation + storage costs for input objects - storage_rebates)
484        ///     if the gas balance is still insufficient, we fall back to set computation_cost = gas_budget
485        ///     so we charge net (gas_budget - storage_rebates)
486        fn compute_storage_and_rebate<T, E: ExecutionErrorTrait>(
487            &mut self,
488            temporary_store: &mut TemporaryStore<'_>,
489            execution_result: &mut Result<T, E>,
490        ) {
491            if dont_charge_budget_on_storage_oog(self.gas_model_version) {
492                self.handle_storage_and_rebate_v2(temporary_store, execution_result)
493            } else {
494                self.handle_storage_and_rebate_v1(temporary_store, execution_result)
495            }
496        }
497
498        fn handle_storage_and_rebate_v1<T, E: ExecutionErrorTrait>(
499            &mut self,
500            temporary_store: &mut TemporaryStore<'_>,
501            execution_result: &mut Result<T, E>,
502        ) {
503            if let Err(err) = self.gas_status.charge_storage_and_rebate() {
504                self.reset(temporary_store);
505                self.gas_status.adjust_computation_on_out_of_gas();
506                temporary_store.ensure_active_inputs_mutated();
507                temporary_store.collect_rebate(self);
508                if execution_result.is_ok() {
509                    *execution_result = Err(err.into());
510                }
511            }
512        }
513
514        fn handle_storage_and_rebate_v2<T, E: ExecutionErrorTrait>(
515            &mut self,
516            temporary_store: &mut TemporaryStore<'_>,
517            execution_result: &mut Result<T, E>,
518        ) {
519            if let Err(err) = self.gas_status.charge_storage_and_rebate() {
520                // we run out of gas charging storage, reset and try charging for storage again.
521                // Input objects are touched and so they have a storage cost
522                // Attempt to charge just for computation + input object storage costs - storage_rebate
523                self.reset(temporary_store);
524                temporary_store.ensure_active_inputs_mutated();
525                temporary_store.collect_storage_and_rebate(self);
526                if let Err(err) = self.gas_status.charge_storage_and_rebate() {
527                    // we run out of gas attempting to charge for the input objects exclusively,
528                    // deal with this edge case by not charging for storage: we charge (gas_budget - rebates).
529                    self.reset(temporary_store);
530                    self.gas_status.adjust_computation_on_out_of_gas();
531                    temporary_store.ensure_active_inputs_mutated();
532                    temporary_store.collect_rebate(self);
533                    if execution_result.is_ok() {
534                        *execution_result = Err(err.into());
535                    }
536                } else if execution_result.is_ok() {
537                    *execution_result = Err(err.into());
538                }
539            }
540        }
541    }
542
543    impl SmashMetadata {
544        /// Iterates over all payment methods: the smash target followed by the smashed payments.
545        fn payment_methods(&self) -> impl Iterator<Item = &'_ PaymentMethod> {
546            std::iter::once(&self.smash_target).chain(self.smashed_payments.values())
547        }
548
549        fn smash_gas(
550            &mut self,
551            tx_digest: &TransactionDigest,
552            temporary_store: &mut TemporaryStore<'_>,
553        ) {
554            // set gas charge location
555            self.gas_charge_location = self.smash_target.location();
556
557            // sum the value of all gas coins
558            let total_smashed = self
559                .payment_methods()
560                .map(|payment| match payment {
561                    PaymentMethod::AddressBalance(_, reservation) => Ok(*reservation),
562                    PaymentMethod::Coin(obj_ref) => {
563                        let obj_data = temporary_store
564                            .objects()
565                            .get(&obj_ref.0)
566                            .map(|obj| &obj.data);
567                        let Some(Data::Move(move_obj)) = obj_data else {
568                            return Err(ExecutionError::invariant_violation(
569                                "Provided non-gas coin object as input for gas!",
570                            ));
571                        };
572                        if !move_obj.type_().is_gas_coin() {
573                            return Err(ExecutionError::invariant_violation(
574                                "Provided non-gas coin object as input for gas!",
575                            ));
576                        }
577                        Ok(move_obj.get_coin_value_unsafe())
578                    }
579                })
580                .collect::<Result<Vec<u64>, ExecutionError>>()
581                // transaction and certificate input checks must have insured that all gas coins
582                // are valid
583                .unwrap_or_else(|_| {
584                    panic!(
585                        "Unable to process gas payments for transaction {}",
586                        tx_digest
587                    )
588                })
589                .iter()
590                .sum();
591            // If it is 0, then we are smashing for the first time (at the beginning of execution).
592            // If it is non-zero, then we are re-smashing after a reset (due to some sort of
593            // failure in charging for gas), and the total should not change.
594            debug_assert!(
595                self.total_smashed == 0 || self.total_smashed == total_smashed,
596                "Gas smashing should not change after a reset"
597            );
598            self.total_smashed = total_smashed;
599
600            let smash_location = self.smash_target.location();
601            // delete all gas objects except the smash target
602            for payment_method in self.smashed_payments.values() {
603                let location = payment_method.location();
604                assert_ne!(location, smash_location, "Payment methods must be unique");
605                match payment_method {
606                    PaymentMethod::AddressBalance(sui_address, reservation) => {
607                        let balance_type = sui_types::balance::Balance::type_tag(
608                            sui_types::gas_coin::GAS::type_tag(),
609                        );
610                        let event = AccumulatorEvent::from_balance_change(
611                            *sui_address,
612                            balance_type,
613                            i64::try_from(*reservation).unwrap().checked_neg().unwrap(),
614                        )
615                        .expect("Failed to create accumulator event for gas smashing");
616                        temporary_store.add_accumulator_event(event);
617                    }
618                    PaymentMethod::Coin((id, _, _)) => {
619                        temporary_store.delete_input_object(id);
620                    }
621                }
622            }
623            match &self.smash_target {
624                PaymentMethod::AddressBalance(sui_address, reservation) => {
625                    // The reservation here is only a maximal withdrawal from this address balance
626                    // We do not need to withdraw here unless necessary, which will be done during
627                    // gas charging
628                    let deposit = total_smashed - *reservation;
629                    if deposit != 0 {
630                        let balance_type = sui_types::balance::Balance::type_tag(
631                            sui_types::gas_coin::GAS::type_tag(),
632                        );
633                        let event = AccumulatorEvent::from_balance_change(
634                            *sui_address,
635                            balance_type,
636                            i64::try_from(deposit).unwrap(),
637                        )
638                        .expect("Failed to create accumulator event for gas smashing");
639                        temporary_store.add_accumulator_event(event);
640                    }
641                }
642                PaymentMethod::Coin((gas_coin_id, _, _)) => {
643                    let mut primary_gas_object = temporary_store
644                        .objects()
645                        .get(gas_coin_id)
646                        // unwrap should be safe because we checked that this exists in `self.objects()` above
647                        .unwrap_or_else(|| {
648                            panic!(
649                                "Invariant violation: gas coin not found in store in txn {}",
650                                tx_digest
651                            )
652                        })
653                        .clone();
654                    primary_gas_object
655                        .data
656                        .try_as_move_mut()
657                        // unwrap should be safe because we checked that the primary gas object was a coin object above.
658                        .unwrap_or_else(|| {
659                            panic!(
660                                "Invariant violation: invalid coin object in txn {}",
661                                tx_digest
662                            )
663                        })
664                        .set_coin_value_unsafe(total_smashed);
665                    temporary_store.mutate_input_object(primary_gas_object);
666                }
667            }
668        }
669
670        fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
671            self.payment_methods().filter_map(|method| match method {
672                PaymentMethod::Coin(obj_ref) => Some(obj_ref),
673                PaymentMethod::AddressBalance(_, _) => None,
674            })
675        }
676    }
677
678    impl PaymentKind {
679        pub fn unmetered() -> Self {
680            Self(PaymentKind_::Unmetered)
681        }
682
683        pub fn gasless() -> Self {
684            Self(PaymentKind_::Gasless)
685        }
686
687        pub fn smash(payment_methods: Vec<PaymentMethod>) -> Option<Self> {
688            debug_assert!(
689                !payment_methods.is_empty(),
690                "GasCharger must have at least one payment method"
691            );
692            if payment_methods.is_empty() {
693                return None;
694            }
695            let mut unique_methods = IndexMap::new();
696            for payment_method in payment_methods {
697                match (
698                    unique_methods.entry(payment_method.location()),
699                    payment_method,
700                ) {
701                    (indexmap::map::Entry::Vacant(entry), payment_method) => {
702                        entry.insert(payment_method);
703                    }
704                    (
705                        indexmap::map::Entry::Occupied(mut occupied),
706                        PaymentMethod::AddressBalance(other, additional),
707                    ) => {
708                        let PaymentMethod::AddressBalance(addr, amount) = occupied.get_mut() else {
709                            unreachable!("Payment method does not match location")
710                        };
711                        assert_eq!(*addr, other, "Payment method does not match location");
712                        *amount += additional;
713                    }
714                    (indexmap::map::Entry::Occupied(_), _) => {
715                        debug_assert!(
716                            false,
717                            "Duplicate coin payment method found, \
718                             which should have been prevented by input checks"
719                        );
720                        return None;
721                    }
722                }
723            }
724            Some(Self(PaymentKind_::Smash(unique_methods)))
725        }
726    }
727
728    impl PaymentMethod {
729        pub fn location(&self) -> PaymentLocation {
730            match self {
731                PaymentMethod::Coin(obj_ref) => PaymentLocation::Coin(obj_ref.0),
732                PaymentMethod::AddressBalance(addr, _) => PaymentLocation::AddressBalance(*addr),
733            }
734        }
735    }
736}