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