casper_storage/system/
transfer.rs

1use std::{cell::RefCell, convert::TryFrom, rc::Rc};
2use thiserror::Error;
3
4use casper_types::{
5    account::AccountHash,
6    bytesrepr::FromBytes,
7    system::{mint, mint::Error as MintError},
8    AccessRights, CLType, CLTyped, CLValue, CLValueError, Key, ProtocolVersion, RuntimeArgs,
9    RuntimeFootprint, StoredValue, StoredValueTypeMismatch, URef, U512,
10};
11
12use crate::{
13    global_state::{error::Error as GlobalStateError, state::StateReader},
14    tracking_copy::{TrackingCopy, TrackingCopyEntityExt, TrackingCopyError, TrackingCopyExt},
15};
16
17/// Transfer error.
18#[derive(Clone, Error, Debug)]
19pub enum TransferError {
20    /// Invalid key variant.
21    #[error("Invalid key {0}")]
22    UnexpectedKeyVariant(Key),
23    /// Type mismatch error.
24    #[error("{}", _0)]
25    TypeMismatch(StoredValueTypeMismatch),
26    /// Forged reference error.
27    #[error("Forged reference: {}", _0)]
28    ForgedReference(URef),
29    /// Invalid access.
30    #[error("Invalid access rights: {}", required)]
31    InvalidAccess {
32        /// Required access rights of the operation.
33        required: AccessRights,
34    },
35    /// Error converting a CLValue.
36    #[error("{0}")]
37    CLValue(CLValueError),
38    /// Invalid purse.
39    #[error("Invalid purse")]
40    InvalidPurse,
41    /// Invalid argument.
42    #[error("Invalid argument")]
43    InvalidArgument,
44    /// Missing argument.
45    #[error("Missing argument")]
46    MissingArgument,
47    /// Invalid purse.
48    #[error("Attempt to transfer amount 0")]
49    AttemptToTransferZero,
50    /// Invalid operation.
51    #[error("Invalid operation")]
52    InvalidOperation,
53    /// Disallowed transfer attempt (private chain).
54    #[error("Either the source or the target must be an admin (private chain).")]
55    RestrictedTransferAttempted,
56    /// Could not determine if target is an admin (private chain).
57    #[error("Unable to determine if the target of a transfer is an admin")]
58    UnableToVerifyTargetIsAdmin,
59    /// Tracking copy error.
60    #[error("{0}")]
61    TrackingCopy(TrackingCopyError),
62    /// Mint error.
63    #[error("{0}")]
64    Mint(MintError),
65}
66
67impl From<GlobalStateError> for TransferError {
68    fn from(gse: GlobalStateError) -> Self {
69        TransferError::TrackingCopy(TrackingCopyError::Storage(gse))
70    }
71}
72
73impl From<TrackingCopyError> for TransferError {
74    fn from(tce: TrackingCopyError) -> Self {
75        TransferError::TrackingCopy(tce)
76    }
77}
78
79/// A target mode indicates if a native transfer's arguments will resolve to an existing purse, or
80/// will have to create a new account first.
81#[derive(Copy, Clone, Debug, PartialEq)]
82pub enum TransferTargetMode {
83    /// Native transfer arguments resolved into a transfer to an existing account.
84    ExistingAccount {
85        /// Existing account hash.
86        target_account_hash: AccountHash,
87        /// Main purse of a resolved account.
88        main_purse: URef,
89    },
90    /// Native transfer arguments resolved into a transfer to a purse.
91    PurseExists {
92        /// Target account hash (if known).
93        target_account_hash: Option<AccountHash>,
94        /// Purse.
95        purse_uref: URef,
96    },
97    /// Native transfer arguments resolved into a transfer to a new account.
98    CreateAccount(AccountHash),
99}
100
101impl TransferTargetMode {
102    /// Target account hash, if any.
103    pub fn target_account_hash(&self) -> Option<AccountHash> {
104        match self {
105            TransferTargetMode::PurseExists {
106                target_account_hash,
107                ..
108            } => *target_account_hash,
109            TransferTargetMode::ExistingAccount {
110                target_account_hash,
111                ..
112            } => Some(*target_account_hash),
113            TransferTargetMode::CreateAccount(target_account_hash) => Some(*target_account_hash),
114        }
115    }
116}
117
118/// Mint's transfer arguments.
119///
120/// A struct has a benefit of static typing, which is helpful while resolving the arguments.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct TransferArgs {
123    to: Option<AccountHash>,
124    source: URef,
125    target: URef,
126    amount: U512,
127    arg_id: Option<u64>,
128}
129
130impl TransferArgs {
131    /// Creates new transfer arguments.
132    pub fn new(
133        to: Option<AccountHash>,
134        source: URef,
135        target: URef,
136        amount: U512,
137        arg_id: Option<u64>,
138    ) -> Self {
139        Self {
140            to,
141            source,
142            target,
143            amount,
144            arg_id,
145        }
146    }
147
148    /// Returns `to` field.
149    pub fn to(&self) -> Option<AccountHash> {
150        self.to
151    }
152
153    /// Returns `source` field.
154    pub fn source(&self) -> URef {
155        self.source
156    }
157
158    /// Returns `target` field.
159    pub fn target(&self) -> URef {
160        self.target
161    }
162
163    /// Returns `amount` field.
164    pub fn amount(&self) -> U512 {
165        self.amount
166    }
167
168    /// Returns `arg_id` field.
169    pub fn arg_id(&self) -> Option<u64> {
170        self.arg_id
171    }
172}
173
174impl TryFrom<TransferArgs> for RuntimeArgs {
175    type Error = CLValueError;
176
177    fn try_from(transfer_args: TransferArgs) -> Result<Self, Self::Error> {
178        let mut runtime_args = RuntimeArgs::new();
179
180        runtime_args.insert(mint::ARG_TO, transfer_args.to)?;
181        runtime_args.insert(mint::ARG_SOURCE, transfer_args.source)?;
182        runtime_args.insert(mint::ARG_TARGET, transfer_args.target)?;
183        runtime_args.insert(mint::ARG_AMOUNT, transfer_args.amount)?;
184        runtime_args.insert(mint::ARG_ID, transfer_args.arg_id)?;
185
186        Ok(runtime_args)
187    }
188}
189
190/// State of a builder of a `TransferArgs`.
191///
192/// Purpose of this builder is to resolve native transfer args into [`TransferTargetMode`] and a
193/// [`TransferArgs`] instance to execute actual token transfer on the mint contract.
194#[derive(Clone, Debug, PartialEq, Eq)]
195pub struct TransferRuntimeArgsBuilder {
196    inner: RuntimeArgs,
197}
198
199impl TransferRuntimeArgsBuilder {
200    /// Creates new transfer args builder.
201    ///
202    /// Takes an incoming runtime args that represents native transfer's arguments.
203    pub fn new(imputed_runtime_args: RuntimeArgs) -> TransferRuntimeArgsBuilder {
204        TransferRuntimeArgsBuilder {
205            inner: imputed_runtime_args,
206        }
207    }
208
209    /// Checks if a purse exists.
210    fn purse_exists<R>(&self, uref: URef, tracking_copy: Rc<RefCell<TrackingCopy<R>>>) -> bool
211    where
212        R: StateReader<Key, StoredValue, Error = GlobalStateError>,
213    {
214        let key = match tracking_copy
215            .borrow_mut()
216            .get_purse_balance_key(uref.into())
217        {
218            Ok(key) => key,
219            Err(_) => return false,
220        };
221        tracking_copy
222            .borrow_mut()
223            .get_available_balance(key)
224            .is_ok()
225    }
226
227    /// Resolves the source purse of the transfer.
228    ///
229    /// User can optionally pass a "source" argument which should refer to an [`URef`] existing in
230    /// user's named keys. When the "source" argument is missing then user's main purse is assumed.
231    ///
232    /// Returns resolved [`URef`].
233    fn resolve_source_uref<R>(
234        &self,
235        account: &RuntimeFootprint,
236        tracking_copy: Rc<RefCell<TrackingCopy<R>>>,
237    ) -> Result<URef, TransferError>
238    where
239        R: StateReader<Key, StoredValue, Error = GlobalStateError>,
240    {
241        let imputed_runtime_args = &self.inner;
242        let arg_name = mint::ARG_SOURCE;
243        let uref = match imputed_runtime_args.get(arg_name) {
244            Some(cl_value) if *cl_value.cl_type() == CLType::URef => {
245                self.map_cl_value::<URef>(cl_value)?
246            }
247            Some(cl_value) if *cl_value.cl_type() == CLType::Option(CLType::URef.into()) => {
248                let Some(uref): Option<URef> = self.map_cl_value(cl_value)? else {
249                    return account.main_purse().ok_or(TransferError::InvalidOperation);
250                };
251                uref
252            }
253            Some(_) => return Err(TransferError::InvalidArgument),
254            None => return account.main_purse().ok_or(TransferError::InvalidOperation), /* if no source purse passed use account
255                                                                                         * main purse */
256        };
257        if account
258            .main_purse()
259            .ok_or(TransferError::InvalidOperation)?
260            .addr()
261            == uref.addr()
262        {
263            return Ok(uref);
264        }
265
266        let normalized_uref = Key::URef(uref).normalize();
267        let maybe_named_key = account
268            .named_keys()
269            .keys()
270            .find(|&named_key| named_key.normalize() == normalized_uref);
271
272        match maybe_named_key {
273            Some(Key::URef(found_uref)) => {
274                if found_uref.is_writeable() {
275                    // it is a URef and caller has access but is it a purse URef?
276                    if !self.purse_exists(found_uref.to_owned(), tracking_copy) {
277                        return Err(TransferError::InvalidPurse);
278                    }
279
280                    Ok(uref)
281                } else {
282                    Err(TransferError::InvalidAccess {
283                        required: AccessRights::WRITE,
284                    })
285                }
286            }
287            Some(key) => Err(TransferError::TypeMismatch(StoredValueTypeMismatch::new(
288                "Key::URef".to_string(),
289                key.type_string(),
290            ))),
291            None => Err(TransferError::ForgedReference(uref)),
292        }
293    }
294
295    /// Resolves a transfer target mode.
296    ///
297    /// User has to specify a "target" argument which must be one of the following types:
298    ///   * an existing purse [`URef`]
299    ///   * a 32-byte array, interpreted as an account hash
300    ///   * a [`Key::Account`], from which the account hash is extracted
301    ///   * a [`casper_types::PublicKey`], which is converted to an account hash
302    ///
303    /// If the "target" account hash is not existing, then a special variant is returned that
304    /// indicates that the system has to create new account first.
305    ///
306    /// Returns [`TransferTargetMode`] with a resolved variant.
307    pub fn resolve_transfer_target_mode<R>(
308        &mut self,
309        protocol_version: ProtocolVersion,
310        tracking_copy: Rc<RefCell<TrackingCopy<R>>>,
311    ) -> Result<TransferTargetMode, TransferError>
312    where
313        R: StateReader<Key, StoredValue, Error = GlobalStateError>,
314    {
315        let imputed_runtime_args = &self.inner;
316        let to_name = mint::ARG_TO;
317
318        let target_account_hash = match imputed_runtime_args.get(to_name) {
319            Some(cl_value)
320                if *cl_value.cl_type() == CLType::Option(Box::new(CLType::ByteArray(32))) =>
321            {
322                let to: Option<AccountHash> = self.map_cl_value(cl_value)?;
323                to
324            }
325            Some(_) | None => None,
326        };
327
328        let target_name = mint::ARG_TARGET;
329        let account_hash = match imputed_runtime_args.get(target_name) {
330            Some(cl_value) if *cl_value.cl_type() == CLType::URef => {
331                let purse_uref = self.map_cl_value(cl_value)?;
332
333                if !self.purse_exists(purse_uref, tracking_copy) {
334                    return Err(TransferError::InvalidPurse);
335                }
336
337                return Ok(TransferTargetMode::PurseExists {
338                    purse_uref,
339                    target_account_hash,
340                });
341            }
342            Some(cl_value) if *cl_value.cl_type() == CLType::ByteArray(32) => {
343                self.map_cl_value(cl_value)?
344            }
345            Some(cl_value) if *cl_value.cl_type() == CLType::Key => {
346                let account_key: Key = self.map_cl_value(cl_value)?;
347                let account_hash: AccountHash = account_key
348                    .into_account()
349                    .ok_or(TransferError::UnexpectedKeyVariant(account_key))?;
350                account_hash
351            }
352            Some(cl_value) if *cl_value.cl_type() == CLType::PublicKey => {
353                let public_key = self.map_cl_value(cl_value)?;
354                AccountHash::from(&public_key)
355            }
356            Some(_) => return Err(TransferError::InvalidArgument),
357            None => return Err(TransferError::MissingArgument),
358        };
359
360        match tracking_copy
361            .borrow_mut()
362            .runtime_footprint_by_account_hash(protocol_version, account_hash)
363        {
364            Ok((_, entity)) => {
365                let main_purse_addable = entity
366                    .main_purse()
367                    .ok_or(TransferError::InvalidPurse)?
368                    .with_access_rights(AccessRights::ADD);
369                Ok(TransferTargetMode::ExistingAccount {
370                    target_account_hash: account_hash,
371                    main_purse: main_purse_addable,
372                })
373            }
374            Err(_) => Ok(TransferTargetMode::CreateAccount(account_hash)),
375        }
376    }
377
378    /// Resolves amount.
379    ///
380    /// User has to specify "amount" argument that could be either a [`U512`] or a u64.
381    fn resolve_amount(&self) -> Result<U512, TransferError> {
382        let imputed_runtime_args = &self.inner;
383
384        let amount = match imputed_runtime_args.get(mint::ARG_AMOUNT) {
385            Some(amount_value) if *amount_value.cl_type() == CLType::U512 => {
386                self.map_cl_value(amount_value)?
387            }
388            Some(amount_value) if *amount_value.cl_type() == CLType::U64 => {
389                let amount: u64 = self.map_cl_value(amount_value)?;
390                U512::from(amount)
391            }
392            Some(_) => return Err(TransferError::InvalidArgument),
393            None => return Err(TransferError::MissingArgument),
394        };
395
396        if amount.is_zero() {
397            return Err(TransferError::AttemptToTransferZero);
398        }
399
400        Ok(amount)
401    }
402
403    fn resolve_id(&self) -> Result<Option<u64>, TransferError> {
404        let id: Option<u64> = if let Some(id_value) = self.inner.get(mint::ARG_ID) {
405            self.map_cl_value(id_value)?
406        } else {
407            None
408        };
409        Ok(id)
410    }
411
412    /// Creates new [`TransferArgs`] instance.
413    pub fn build<R>(
414        mut self,
415        from: &RuntimeFootprint,
416        protocol_version: ProtocolVersion,
417        tracking_copy: Rc<RefCell<TrackingCopy<R>>>,
418    ) -> Result<TransferArgs, TransferError>
419    where
420        R: StateReader<Key, StoredValue, Error = GlobalStateError>,
421    {
422        let (to, target) = match self
423            .resolve_transfer_target_mode(protocol_version, Rc::clone(&tracking_copy))?
424        {
425            TransferTargetMode::ExistingAccount {
426                main_purse: purse_uref,
427                target_account_hash: target_account,
428            } => (Some(target_account), purse_uref),
429            TransferTargetMode::PurseExists {
430                target_account_hash,
431                purse_uref,
432            } => (target_account_hash, purse_uref),
433            TransferTargetMode::CreateAccount(_) => {
434                // Method "build()" is called after `resolve_transfer_target_mode` is first called
435                // and handled by creating a new account. Calling `resolve_transfer_target_mode`
436                // for the second time should never return `CreateAccount` variant.
437                return Err(TransferError::InvalidOperation);
438            }
439        };
440
441        let source = self.resolve_source_uref(from, Rc::clone(&tracking_copy))?;
442
443        if source.addr() == target.addr() {
444            return Err(TransferError::InvalidPurse);
445        }
446
447        let amount = self.resolve_amount()?;
448
449        let arg_id = self.resolve_id()?;
450
451        Ok(TransferArgs {
452            to,
453            source,
454            target,
455            amount,
456            arg_id,
457        })
458    }
459
460    fn map_cl_value<T: CLTyped + FromBytes>(&self, cl_value: &CLValue) -> Result<T, TransferError> {
461        cl_value.clone().into_t().map_err(TransferError::CLValue)
462    }
463}