Skip to main content

sui_move_natives_latest/
funds_accumulator.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::VecDeque;
5
6use move_binary_format::{
7    errors::{PartialVMError, PartialVMResult},
8    safe_assert_eq, safe_unwrap,
9};
10use move_core_types::{
11    account_address::AccountAddress, gas_algebra::InternalGas, u256::U256, vm_status::StatusCode,
12};
13use move_vm_runtime::{
14    execution::{
15        Type,
16        values::{Struct, Value},
17    },
18    native_charge_gas_early_exit,
19    natives::functions::{NativeContext, NativeResult},
20};
21use smallvec::smallvec;
22use sui_types::{
23    accumulator_root::check_accumulator_type_bounds,
24    base_types::{ObjectID, SuiAddress},
25    funds_accumulator::E_OBJECT_FUNDS_INSUFFICIENT,
26    storage::ObjectFundsSufficiency,
27};
28
29use crate::{
30    NativesCostTable,
31    object_runtime::{MoveAccumulatorAction, MoveAccumulatorValue, ObjectRuntime},
32};
33
34const E_OVERFLOW: u64 = 0;
35const E_ADDRESS_BALANCE_NOT_ENABLED: u64 = 1;
36const E_ACCUMULATOR_TYPE_TOO_LARGE: u64 = 4;
37
38#[derive(Clone)]
39pub struct ReserveObjectFundsForWithdrawalCostParams {
40    pub base_cost: Option<InternalGas>,
41    pub cold_read_cost: Option<InternalGas>,
42}
43
44pub fn add_to_accumulator_address(
45    context: &mut NativeContext,
46    mut ty_args: Vec<Type>,
47    mut args: VecDeque<Value>,
48) -> PartialVMResult<NativeResult> {
49    debug_assert!(ty_args.len() == 1);
50    debug_assert!(args.len() == 3);
51
52    // TODO(address-balances): add specific cost for this
53    let event_emit_cost_params = context
54        .extensions_mut()
55        .get::<NativesCostTable>()?
56        .event_emit_cost_params
57        .clone();
58    native_charge_gas_early_exit!(context, event_emit_cost_params.event_emit_cost_base);
59
60    let ty_tag = context.type_to_type_tag(&safe_unwrap!(ty_args.pop()))?;
61
62    let Some(value) = safe_unwrap!(args.pop_back()).value_as::<Struct>().ok() else {
63        // TODO in the future this is guaranteed/checked via a custom verifier rule
64        debug_assert!(false);
65        return Err(
66            PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message(
67                "Balance should be guaranteed under current implementation".to_owned(),
68            ),
69        );
70    };
71    let recipient = safe_unwrap!(safe_unwrap!(args.pop_back()).value_as::<AccountAddress>());
72    let accumulator: ObjectID =
73        safe_unwrap!(safe_unwrap!(args.pop_back()).value_as::<AccountAddress>()).into();
74
75    // TODO this will need to look at the layout of T when this is not guaranteed to be a Balance
76    let Some([amount]): Option<[Value; 1]> = value.unpack().collect::<Vec<_>>().try_into().ok()
77    else {
78        debug_assert!(false);
79        return Err(
80            PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message(
81                "Balance should be guaranteed under current implementation".to_owned(),
82            ),
83        );
84    };
85    let Some(amount) = amount.value_as::<u64>().ok() else {
86        debug_assert!(false);
87        return Err(
88            PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message(
89                "Balance should be guaranteed under current implementation".to_owned(),
90            ),
91        );
92    };
93
94    let cost = context.gas_used();
95
96    let obj_runtime: &mut ObjectRuntime = context.extensions_mut().get_mut()?;
97
98    if !obj_runtime.protocol_config.enable_accumulators() {
99        return Ok(NativeResult::err(cost, E_ADDRESS_BALANCE_NOT_ENABLED));
100    }
101
102    if !check_accumulator_type_bounds(obj_runtime.protocol_config, &ty_tag) {
103        return Ok(NativeResult::err(cost, E_ACCUMULATOR_TYPE_TOO_LARGE));
104    }
105
106    obj_runtime.emit_accumulator_event(
107        accumulator,
108        MoveAccumulatorAction::Merge,
109        recipient,
110        ty_tag,
111        MoveAccumulatorValue::U64(amount),
112    )?;
113    Ok(NativeResult::ok(context.gas_used(), smallvec![]))
114}
115
116pub fn withdraw_from_accumulator_address(
117    context: &mut NativeContext,
118    mut ty_args: Vec<Type>,
119    mut args: VecDeque<Value>,
120) -> PartialVMResult<NativeResult> {
121    debug_assert!(ty_args.len() == 1);
122    debug_assert!(args.len() == 3);
123
124    // TODO(address-balances): add specific cost for this
125    // TODO(address-balances): determine storage cost for "Merge"
126    let event_emit_cost_params = context
127        .extensions_mut()
128        .get::<NativesCostTable>()?
129        .event_emit_cost_params
130        .clone();
131    native_charge_gas_early_exit!(context, event_emit_cost_params.event_emit_cost_base);
132
133    let ty_tag = context.type_to_type_tag(&safe_unwrap!(ty_args.pop()))?;
134
135    let value = safe_unwrap!(safe_unwrap!(args.pop_back()).value_as::<U256>());
136    let recipient = safe_unwrap!(safe_unwrap!(args.pop_back()).value_as::<AccountAddress>());
137    let accumulator: ObjectID =
138        safe_unwrap!(safe_unwrap!(args.pop_back()).value_as::<AccountAddress>()).into();
139
140    // TODO this will need to look at the layout of T when this is not guaranteed to be a Balance
141    let Ok(amount): Result<u64, _> = value.try_into() else {
142        return Ok(NativeResult::err(context.gas_used(), E_OVERFLOW));
143    };
144
145    let cost = context.gas_used();
146
147    let obj_runtime: &mut ObjectRuntime = context.extensions_mut().get_mut()?;
148
149    if !check_accumulator_type_bounds(obj_runtime.protocol_config, &ty_tag) {
150        return Ok(NativeResult::err(cost, E_ACCUMULATOR_TYPE_TOO_LARGE));
151    }
152
153    obj_runtime.emit_accumulator_event(
154        accumulator,
155        MoveAccumulatorAction::Split,
156        recipient,
157        ty_tag,
158        MoveAccumulatorValue::U64(amount),
159    )?;
160    // TODO this will need to look at the layout of T when this is not guaranteed to be a Balance
161    let withdrawn = Value::struct_(Struct::pack(vec![Value::u64(amount)]));
162    Ok(NativeResult::ok(context.gas_used(), smallvec![withdrawn]))
163}
164
165pub fn reserve_object_funds_for_withdrawal(
166    context: &mut NativeContext,
167    mut ty_args: Vec<Type>,
168    mut args: VecDeque<Value>,
169) -> PartialVMResult<NativeResult> {
170    safe_assert_eq!(ty_args.len(), 1);
171    safe_assert_eq!(args.len(), 2);
172
173    let (ty_tag, limit, owner) = {
174        let obj_runtime: &ObjectRuntime = context.extensions().get()?;
175        if !obj_runtime
176            .protocol_config
177            .check_object_funds_withdraw_in_execution()
178        {
179            // Before check_object_funds_withdraw_in_execution is enabled, object funds withdrawals are not
180            // checked in execution. They are checked post-execution, and potentially retried if insufficient.
181            return Ok(NativeResult::ok(context.gas_used(), smallvec![]));
182        }
183
184        let cost_params = context
185            .extensions()
186            .get::<NativesCostTable>()?
187            .reserve_object_funds_for_withdrawal_cost_params
188            .clone();
189        let base_cost = cost_params.base_cost.ok_or_else(|| {
190            PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message(
191                "reserve_object_funds_for_withdrawal base gas cost is not set".to_string(),
192            )
193        })?;
194        native_charge_gas_early_exit!(context, base_cost);
195
196        let ty_tag = context.type_to_type_tag(&safe_unwrap!(ty_args.pop()))?;
197        let limit = safe_unwrap!(safe_unwrap!(args.pop_back()).value_as::<U256>());
198        let owner: SuiAddress =
199            safe_unwrap!(safe_unwrap!(args.pop_back()).value_as::<AccountAddress>()).into();
200
201        // We want to charge extra gas if we need to read the object available funds from storage.
202        // This should only need to be done once per account.
203        // Check here so that we can charge gas before reading from storage.
204        if obj_runtime.object_funds_sufficiency_needs_store_read(owner, &ty_tag, limit) {
205            let cold_read_cost = cost_params.cold_read_cost.ok_or_else(|| {
206                PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR).with_message(
207                    "reserve_object_funds_for_withdrawal cold read gas cost is not set".to_string(),
208                )
209            })?;
210            native_charge_gas_early_exit!(context, cold_read_cost);
211        }
212        (ty_tag, limit, owner)
213    };
214
215    let obj_runtime: &mut ObjectRuntime = context.extensions_mut().get_mut()?;
216    match obj_runtime.check_object_funds_sufficiency(owner, &ty_tag, limit) {
217        ObjectFundsSufficiency::Sufficient => Ok(NativeResult::ok(context.gas_used(), smallvec![])),
218        ObjectFundsSufficiency::Insufficient => Ok(NativeResult::err(
219            context.gas_used(),
220            E_OBJECT_FUNDS_INSUFFICIENT,
221        )),
222        ObjectFundsSufficiency::Overflow => Ok(NativeResult::err(context.gas_used(), E_OVERFLOW)),
223        ObjectFundsSufficiency::LoadError(msg) => Err(PartialVMError::new(
224            StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR,
225        )
226        .with_message(msg)),
227    }
228}