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,
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 fn gas_budget(&self) -> u64 {
206            self.gas_status.gas_budget()
207        }
208
209        pub fn unmetered_storage_rebate(&self) -> u64 {
210            self.gas_status.unmetered_storage_rebate()
211        }
212
213        pub fn no_charges(&self) -> bool {
214            self.gas_status.gas_used() == 0
215                && self.gas_status.storage_rebate() == 0
216                && self.gas_status.storage_gas_units() == 0
217        }
218
219        pub fn is_unmetered(&self) -> bool {
220            self.gas_status.is_unmetered()
221        }
222
223        pub fn move_gas_status(&self) -> &GasStatus {
224            self.gas_status.move_gas_status()
225        }
226
227        pub fn move_gas_status_mut(&mut self) -> &mut GasStatus {
228            self.gas_status.move_gas_status_mut()
229        }
230
231        pub fn into_gas_status(self) -> SuiGasStatus {
232            self.gas_status
233        }
234
235        pub fn summary(&self) -> GasCostSummary {
236            self.gas_status.summary()
237        }
238
239        // This function is called when the transaction is about to be executed.
240        // It will smash all gas coins into a single one and set the logical gas coin
241        // to be the first one in the list.
242        // After this call, `gas_coin` will return it id of the gas coin.
243        // This function panics if errors are found while operation on the gas coins.
244        // Transaction and certificate input checks must have insured that all gas coins
245        // are correct.
246        fn smash_gas(&mut self, temporary_store: &mut TemporaryStore<'_>) {
247            match &mut self.payment {
248                PaymentMetadata::Unmetered | PaymentMetadata::Gasless => (),
249                PaymentMetadata::Smash(smash_metadata) => {
250                    smash_metadata.smash_gas(&self.tx_digest, temporary_store);
251                }
252            }
253        }
254
255        //
256        // Gas charging operations
257        //
258
259        pub fn track_storage_mutation(
260            &mut self,
261            object_id: ObjectID,
262            new_size: usize,
263            storage_rebate: u64,
264        ) -> u64 {
265            self.gas_status
266                .track_storage_mutation(object_id, new_size, storage_rebate)
267        }
268
269        pub fn reset_storage_cost_and_rebate(&mut self) {
270            self.gas_status.reset_storage_cost_and_rebate();
271        }
272
273        pub fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
274            self.gas_status.charge_publish_package(size)
275        }
276
277        pub fn charge_upgrade_package(&mut self, size: usize) -> Result<(), ExecutionError> {
278            if charge_upgrades(self.gas_model_version) {
279                self.gas_status.charge_publish_package(size)
280            } else {
281                Ok(())
282            }
283        }
284
285        pub fn charge_input_objects(
286            &mut self,
287            temporary_store: &TemporaryStore<'_>,
288        ) -> Result<(), ExecutionError> {
289            let objects = temporary_store.objects();
290            // TODO: Charge input object count.
291            let _object_count = objects.len();
292            // Charge bytes read
293            let total_size = temporary_store
294                .objects()
295                .iter()
296                // don't charge for loading Sui Framework or Move stdlib
297                .filter(|(id, _)| !is_system_package(**id))
298                .map(|(_, obj)| obj.object_size_for_gas_metering())
299                .sum();
300            self.gas_status.charge_storage_read(total_size)
301        }
302
303        pub fn charge_coin_transfers(
304            &mut self,
305            protocol_config: &ProtocolConfig,
306            num_non_gas_coin_owners: u64,
307        ) -> Result<(), ExecutionError> {
308            // times two for the global pause and per-address settings
309            // this "overcharges" slightly since it does not check the global pause for each owner
310            // but rather each coin type.
311            let bytes_read_per_owner = CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
312            // associate the cost with dynamic field access so that it will increase if/when this
313            // cost increases
314            let cost_per_byte =
315                protocol_config.dynamic_field_borrow_child_object_type_cost_per_byte() as usize;
316            let cost_per_owner = bytes_read_per_owner * cost_per_byte;
317            let owner_cost = cost_per_owner * (num_non_gas_coin_owners as usize);
318            self.gas_status.charge_storage_read(owner_cost)
319        }
320
321        /// Resets any mutations, deletions, and events recorded in the store, as well as any storage costs and
322        /// rebates, then Re-runs gas smashing. Effects on store are now as if we were about to begin execution
323        pub fn reset(&mut self, temporary_store: &mut TemporaryStore<'_>) {
324            temporary_store.drop_writes();
325            self.gas_status.reset_storage_cost_and_rebate();
326            self.smash_gas(temporary_store);
327        }
328
329        /// Entry point for gas charging.
330        /// 1. Compute tx storage gas costs and tx storage rebates, update storage_rebate field of
331        /// mutated objects
332        /// 2. Deduct computation gas costs and storage costs, credit storage rebates.
333        /// The happy path of this function follows (1) + (2) and is fairly simple.
334        /// Most of the complexity is in the unhappy paths:
335        /// - if execution aborted before calling this function, we have to dump all writes +
336        ///   re-smash gas, then charge for storage
337        /// - if we run out of gas while charging for storage, we have to dump all writes +
338        ///   re-smash gas, then charge for storage again
339        pub fn charge_gas<T, E: ExecutionErrorTrait>(
340            &mut self,
341            temporary_store: &mut TemporaryStore<'_>,
342            execution_result: &mut Result<T, E>,
343        ) -> GasCostSummary {
344            // at this point, we have done *all* charging for computation,
345            // but have not yet set the storage rebate or storage gas units
346            debug_assert!(self.gas_status.storage_rebate() == 0);
347            debug_assert!(self.gas_status.storage_gas_units() == 0);
348
349            if !matches!(&self.payment, PaymentMetadata::Unmetered) {
350                // bucketize computation cost
351                let is_move_abort = execution_result
352                    .as_ref()
353                    .err()
354                    .map(|err| {
355                        matches!(
356                            err.kind(),
357                            sui_types::execution_status::ExecutionErrorKind::MoveAbort(_, _)
358                        )
359                    })
360                    .unwrap_or(false);
361                // bucketize computation cost
362                if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
363                    && execution_result.is_ok()
364                {
365                    *execution_result = Err(err.into());
366                }
367
368                // On error we need to dump writes, deletes, etc before charging storage gas
369                if execution_result.is_err() {
370                    self.reset(temporary_store);
371                }
372            }
373
374            // compute and collect storage charges
375            temporary_store.ensure_active_inputs_mutated();
376            temporary_store.collect_storage_and_rebate(self);
377
378            let gas_payment_location = match &self.payment {
379                PaymentMetadata::Unmetered => {
380                    return GasCostSummary::default();
381                }
382                PaymentMetadata::Gasless => None,
383                PaymentMetadata::Smash(metadata) => Some(metadata.gas_charge_location),
384            };
385            if let Some(PaymentLocation::Coin(_)) = gas_payment_location {
386                #[skip_checked_arithmetic]
387                trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
388            }
389
390            if execution_result
391                .as_ref()
392                .err()
393                .map(|err| {
394                    matches!(
395                        err.kind(),
396                        sui_types::execution_status::ExecutionErrorKind::InsufficientFundsForWithdraw
397                    )
398                })
399                .unwrap_or(false)
400                && matches!(gas_payment_location, Some(PaymentLocation::AddressBalance(_))) {
401                    // If we don't have enough balance to withdraw, don't charge for gas
402                    // TODO: consider charging gas if we have enough to reserve but not enough to cover all withdraws
403                    return GasCostSummary::default();
404            }
405
406            self.compute_storage_and_rebate(temporary_store, execution_result);
407
408            let cost_summary = self.gas_status.summary();
409
410            let Some(gas_payment_location) = gas_payment_location else {
411                // Gasless: sender pays nothing.
412                assert!(
413                    matches!(self.payment, PaymentMetadata::Gasless),
414                    "Only gasless transactions should reach this point without a payment location"
415                );
416                if execution_result.is_err() {
417                    return GasCostSummary::default();
418                }
419                // Any storage rebate from destroyed input coins is absorbed as
420                // network fees, not returned to sender.
421                let storage_cost = cost_summary.storage_cost;
422                assert!(
423                    storage_cost == 0,
424                    "Gasless transaction must not incur storage cost, got {storage_cost}"
425                );
426                let sender_rebate = cost_summary.storage_rebate;
427                return GasCostSummary {
428                    computation_cost: sender_rebate,
429                    storage_cost: 0,
430                    storage_rebate: sender_rebate,
431                    non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
432                };
433            };
434
435            let net_change = cost_summary.net_gas_usage();
436
437            match gas_payment_location {
438                PaymentLocation::AddressBalance(payer_address) => {
439                    // TODO tracing?
440                    if net_change != 0 {
441                        let balance_type = sui_types::balance::Balance::type_tag(
442                            sui_types::gas_coin::GAS::type_tag(),
443                        );
444                        let event = AccumulatorEvent::from_balance_change(
445                            payer_address,
446                            balance_type,
447                            net_change.checked_neg().unwrap(),
448                        )
449                        .expect("Failed to create accumulator event for gas charging");
450                        temporary_store.add_accumulator_event(event);
451                    }
452                }
453                PaymentLocation::Coin(gas_object_id) => {
454                    let mut gas_object =
455                        temporary_store.read_object(&gas_object_id).unwrap().clone();
456                    deduct_gas(&mut gas_object, net_change);
457                    #[skip_checked_arithmetic]
458                    trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
459                    temporary_store.mutate_new_or_input_object(gas_object);
460                }
461            }
462            cost_summary
463        }
464
465        /// Calculate total gas cost considering storage and rebate.
466        ///
467        /// First, we net computation, storage, and rebate to determine total gas to charge.
468        ///
469        /// If we exceed gas_budget, we set execution_result to InsufficientGas, failing the tx.
470        /// If we have InsufficientGas, we determine how much gas to charge for the failed tx:
471        ///
472        /// v1: we set computation_cost = gas_budget, so we charge net (gas_budget - storage_rebates)
473        /// v2: we charge (computation + storage costs for input objects - storage_rebates)
474        ///     if the gas balance is still insufficient, we fall back to set computation_cost = gas_budget
475        ///     so we charge net (gas_budget - storage_rebates)
476        fn compute_storage_and_rebate<T, E: ExecutionErrorTrait>(
477            &mut self,
478            temporary_store: &mut TemporaryStore<'_>,
479            execution_result: &mut Result<T, E>,
480        ) {
481            if dont_charge_budget_on_storage_oog(self.gas_model_version) {
482                self.handle_storage_and_rebate_v2(temporary_store, execution_result)
483            } else {
484                self.handle_storage_and_rebate_v1(temporary_store, execution_result)
485            }
486        }
487
488        fn handle_storage_and_rebate_v1<T, E: ExecutionErrorTrait>(
489            &mut self,
490            temporary_store: &mut TemporaryStore<'_>,
491            execution_result: &mut Result<T, E>,
492        ) {
493            if let Err(err) = self.gas_status.charge_storage_and_rebate() {
494                self.reset(temporary_store);
495                self.gas_status.adjust_computation_on_out_of_gas();
496                temporary_store.ensure_active_inputs_mutated();
497                temporary_store.collect_rebate(self);
498                if execution_result.is_ok() {
499                    *execution_result = Err(err.into());
500                }
501            }
502        }
503
504        fn handle_storage_and_rebate_v2<T, E: ExecutionErrorTrait>(
505            &mut self,
506            temporary_store: &mut TemporaryStore<'_>,
507            execution_result: &mut Result<T, E>,
508        ) {
509            if let Err(err) = self.gas_status.charge_storage_and_rebate() {
510                // we run out of gas charging storage, reset and try charging for storage again.
511                // Input objects are touched and so they have a storage cost
512                // Attempt to charge just for computation + input object storage costs - storage_rebate
513                self.reset(temporary_store);
514                temporary_store.ensure_active_inputs_mutated();
515                temporary_store.collect_storage_and_rebate(self);
516                if let Err(err) = self.gas_status.charge_storage_and_rebate() {
517                    // we run out of gas attempting to charge for the input objects exclusively,
518                    // deal with this edge case by not charging for storage: we charge (gas_budget - rebates).
519                    self.reset(temporary_store);
520                    self.gas_status.adjust_computation_on_out_of_gas();
521                    temporary_store.ensure_active_inputs_mutated();
522                    temporary_store.collect_rebate(self);
523                    if execution_result.is_ok() {
524                        *execution_result = Err(err.into());
525                    }
526                } else if execution_result.is_ok() {
527                    *execution_result = Err(err.into());
528                }
529            }
530        }
531    }
532
533    impl SmashMetadata {
534        /// Iterates over all payment methods: the smash target followed by the smashed payments.
535        fn payment_methods(&self) -> impl Iterator<Item = &'_ PaymentMethod> {
536            std::iter::once(&self.smash_target).chain(self.smashed_payments.values())
537        }
538
539        fn smash_gas(
540            &mut self,
541            tx_digest: &TransactionDigest,
542            temporary_store: &mut TemporaryStore<'_>,
543        ) {
544            // set gas charge location
545            self.gas_charge_location = self.smash_target.location();
546
547            // sum the value of all gas coins
548            let total_smashed = self
549                .payment_methods()
550                .map(|payment| match payment {
551                    PaymentMethod::AddressBalance(_, reservation) => Ok(*reservation),
552                    PaymentMethod::Coin(obj_ref) => {
553                        let obj_data = temporary_store
554                            .objects()
555                            .get(&obj_ref.0)
556                            .map(|obj| &obj.data);
557                        let Some(Data::Move(move_obj)) = obj_data else {
558                            return Err(ExecutionError::invariant_violation(
559                                "Provided non-gas coin object as input for gas!",
560                            ));
561                        };
562                        if !move_obj.type_().is_gas_coin() {
563                            return Err(ExecutionError::invariant_violation(
564                                "Provided non-gas coin object as input for gas!",
565                            ));
566                        }
567                        Ok(move_obj.get_coin_value_unsafe())
568                    }
569                })
570                .collect::<Result<Vec<u64>, ExecutionError>>()
571                // transaction and certificate input checks must have insured that all gas coins
572                // are valid
573                .unwrap_or_else(|_| {
574                    panic!(
575                        "Unable to process gas payments for transaction {}",
576                        tx_digest
577                    )
578                })
579                .iter()
580                .sum();
581            // If it is 0, then we are smashing for the first time (at the beginning of execution).
582            // If it is non-zero, then we are re-smashing after a reset (due to some sort of
583            // failure in charging for gas), and the total should not change.
584            debug_assert!(
585                self.total_smashed == 0 || self.total_smashed == total_smashed,
586                "Gas smashing should not change after a reset"
587            );
588            self.total_smashed = total_smashed;
589
590            let smash_location = self.smash_target.location();
591            // delete all gas objects except the smash target
592            for payment_method in self.smashed_payments.values() {
593                let location = payment_method.location();
594                assert_ne!(location, smash_location, "Payment methods must be unique");
595                match payment_method {
596                    PaymentMethod::AddressBalance(sui_address, reservation) => {
597                        let balance_type = sui_types::balance::Balance::type_tag(
598                            sui_types::gas_coin::GAS::type_tag(),
599                        );
600                        let event = AccumulatorEvent::from_balance_change(
601                            *sui_address,
602                            balance_type,
603                            i64::try_from(*reservation).unwrap().checked_neg().unwrap(),
604                        )
605                        .expect("Failed to create accumulator event for gas smashing");
606                        temporary_store.add_accumulator_event(event);
607                    }
608                    PaymentMethod::Coin((id, _, _)) => {
609                        temporary_store.delete_input_object(id);
610                    }
611                }
612            }
613            match &self.smash_target {
614                PaymentMethod::AddressBalance(sui_address, 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}