miden_lib/account/faucets/
basic_fungible.rs

1use miden_objects::account::{
2    Account,
3    AccountBuilder,
4    AccountComponent,
5    AccountStorage,
6    AccountStorageMode,
7    AccountType,
8    StorageSlot,
9};
10use miden_objects::asset::{FungibleAsset, TokenSymbol};
11use miden_objects::{Felt, FieldElement, Word};
12
13use super::FungibleFaucetError;
14use crate::account::AuthScheme;
15use crate::account::auth::{
16    AuthEcdsaK256KeccakAcl,
17    AuthEcdsaK256KeccakAclConfig,
18    AuthRpoFalcon512Acl,
19    AuthRpoFalcon512AclConfig,
20};
21use crate::account::components::basic_fungible_faucet_library;
22use crate::account::interface::{AccountComponentInterface, AccountInterface};
23use crate::procedure_digest;
24
25// BASIC FUNGIBLE FAUCET ACCOUNT COMPONENT
26// ================================================================================================
27
28// Initialize the digest of the `distribute` procedure of the Basic Fungible Faucet only once.
29procedure_digest!(
30    BASIC_FUNGIBLE_FAUCET_DISTRIBUTE,
31    BasicFungibleFaucet::DISTRIBUTE_PROC_NAME,
32    basic_fungible_faucet_library
33);
34
35// Initialize the digest of the `burn` procedure of the Basic Fungible Faucet only once.
36procedure_digest!(
37    BASIC_FUNGIBLE_FAUCET_BURN,
38    BasicFungibleFaucet::BURN_PROC_NAME,
39    basic_fungible_faucet_library
40);
41
42/// An [`AccountComponent`] implementing a basic fungible faucet.
43///
44/// It reexports the procedures from `miden::contracts::faucets::basic_fungible`. When linking
45/// against this component, the `miden` library (i.e. [`MidenLib`](crate::MidenLib)) must be
46/// available to the assembler which is the case when using
47/// [`TransactionKernel::assembler()`][kasm]. The procedures of this component are:
48/// - `distribute`, which mints an assets and create a note for the provided recipient.
49/// - `burn`, which burns the provided asset.
50///
51/// The `distribute` procedure can be called from a transaction script and requires authentication
52/// via the authentication component. The `burn` procedure can only be called from a note script
53/// and requires the calling note to contain the asset to be burned.
54/// This component must be combined with an authentication component.
55///
56/// This component supports accounts of type [`AccountType::FungibleFaucet`].
57///
58/// [kasm]: crate::transaction::TransactionKernel::assembler
59pub struct BasicFungibleFaucet {
60    symbol: TokenSymbol,
61    decimals: u8,
62    max_supply: Felt,
63}
64
65impl BasicFungibleFaucet {
66    // CONSTANTS
67    // --------------------------------------------------------------------------------------------
68
69    /// The maximum number of decimals supported by the component.
70    pub const MAX_DECIMALS: u8 = 12;
71
72    const DISTRIBUTE_PROC_NAME: &str = "distribute";
73    const BURN_PROC_NAME: &str = "burn";
74
75    // CONSTRUCTORS
76    // --------------------------------------------------------------------------------------------
77
78    /// Creates a new [`BasicFungibleFaucet`] component from the given pieces of metadata.
79    ///
80    /// # Errors:
81    /// Returns an error if:
82    /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`].
83    /// - the max supply parameter exceeds maximum possible amount for a fungible asset
84    ///   ([`FungibleAsset::MAX_AMOUNT`])
85    pub fn new(
86        symbol: TokenSymbol,
87        decimals: u8,
88        max_supply: Felt,
89    ) -> Result<Self, FungibleFaucetError> {
90        // First check that the metadata is valid.
91        if decimals > Self::MAX_DECIMALS {
92            return Err(FungibleFaucetError::TooManyDecimals {
93                actual: decimals as u64,
94                max: Self::MAX_DECIMALS,
95            });
96        } else if max_supply.as_int() > FungibleAsset::MAX_AMOUNT {
97            return Err(FungibleFaucetError::MaxSupplyTooLarge {
98                actual: max_supply.as_int(),
99                max: FungibleAsset::MAX_AMOUNT,
100            });
101        }
102
103        Ok(Self { symbol, decimals, max_supply })
104    }
105
106    /// Attempts to create a new [`BasicFungibleFaucet`] component from the associated account
107    /// interface and storage.
108    ///
109    /// # Errors:
110    /// Returns an error if:
111    /// - the provided [`AccountInterface`] does not contain a
112    ///   [`AccountComponentInterface::BasicFungibleFaucet`] component.
113    /// - the decimals parameter exceeds maximum value of [`Self::MAX_DECIMALS`].
114    /// - the max supply value exceeds maximum possible amount for a fungible asset of
115    ///   [`FungibleAsset::MAX_AMOUNT`].
116    /// - the token symbol encoded value exceeds the maximum value of
117    ///   [`TokenSymbol::MAX_ENCODED_VALUE`].
118    fn try_from_interface(
119        interface: AccountInterface,
120        storage: &AccountStorage,
121    ) -> Result<Self, FungibleFaucetError> {
122        for component in interface.components().iter() {
123            if let AccountComponentInterface::BasicFungibleFaucet(offset) = component {
124                // obtain metadata from storage using offset provided by BasicFungibleFaucet
125                // interface
126                let faucet_metadata = storage
127                    .get_item(*offset)
128                    .map_err(|_| FungibleFaucetError::InvalidStorageOffset(*offset))?;
129                let [max_supply, decimals, token_symbol, _] = *faucet_metadata;
130
131                // verify metadata values
132                let token_symbol = TokenSymbol::try_from(token_symbol)
133                    .map_err(FungibleFaucetError::InvalidTokenSymbol)?;
134                let decimals = decimals.as_int().try_into().map_err(|_| {
135                    FungibleFaucetError::TooManyDecimals {
136                        actual: decimals.as_int(),
137                        max: Self::MAX_DECIMALS,
138                    }
139                })?;
140
141                return BasicFungibleFaucet::new(token_symbol, decimals, max_supply);
142            }
143        }
144
145        Err(FungibleFaucetError::NoAvailableInterface)
146    }
147
148    // PUBLIC ACCESSORS
149    // --------------------------------------------------------------------------------------------
150
151    /// Returns the symbol of the faucet.
152    pub fn symbol(&self) -> TokenSymbol {
153        self.symbol
154    }
155
156    /// Returns the decimals of the faucet.
157    pub fn decimals(&self) -> u8 {
158        self.decimals
159    }
160
161    /// Returns the max supply of the faucet.
162    pub fn max_supply(&self) -> Felt {
163        self.max_supply
164    }
165
166    /// Returns the digest of the `distribute` account procedure.
167    pub fn distribute_digest() -> Word {
168        *BASIC_FUNGIBLE_FAUCET_DISTRIBUTE
169    }
170
171    /// Returns the digest of the `burn` account procedure.
172    pub fn burn_digest() -> Word {
173        *BASIC_FUNGIBLE_FAUCET_BURN
174    }
175}
176
177impl From<BasicFungibleFaucet> for AccountComponent {
178    fn from(faucet: BasicFungibleFaucet) -> Self {
179        // Note: data is stored as [a0, a1, a2, a3] but loaded onto the stack as
180        // [a3, a2, a1, a0, ...]
181        let metadata = Word::new([
182            faucet.max_supply,
183            Felt::from(faucet.decimals),
184            faucet.symbol.into(),
185            Felt::ZERO,
186        ]);
187
188        AccountComponent::new(basic_fungible_faucet_library(), vec![StorageSlot::Value(metadata)])
189            .expect("basic fungible faucet component should satisfy the requirements of a valid account component")
190            .with_supported_type(AccountType::FungibleFaucet)
191    }
192}
193
194impl TryFrom<Account> for BasicFungibleFaucet {
195    type Error = FungibleFaucetError;
196
197    fn try_from(account: Account) -> Result<Self, Self::Error> {
198        let account_interface = AccountInterface::from(&account);
199
200        BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
201    }
202}
203
204impl TryFrom<&Account> for BasicFungibleFaucet {
205    type Error = FungibleFaucetError;
206
207    fn try_from(account: &Account) -> Result<Self, Self::Error> {
208        let account_interface = AccountInterface::from(account);
209
210        BasicFungibleFaucet::try_from_interface(account_interface, account.storage())
211    }
212}
213
214/// Creates a new faucet account with basic fungible faucet interface,
215/// account storage type, specified authentication scheme, and provided meta data (token symbol,
216/// decimals, max supply).
217///
218/// The basic faucet interface exposes two procedures:
219/// - `distribute`, which mints an assets and create a note for the provided recipient.
220/// - `burn`, which burns the provided asset.
221///
222/// The `distribute` procedure can be called from a transaction script and requires authentication
223/// via the specified authentication scheme. The `burn` procedure can only be called from a note
224/// script and requires the calling note to contain the asset to be burned.
225///
226/// The storage layout of the faucet account is:
227/// - Slot 0: Reserved slot for faucets.
228/// - Slot 1: Public Key of the authentication component.
229/// - Slot 2: [num_tracked_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes,
230///   0].
231/// - Slot 3: A map with tracked procedure roots.
232/// - Slot 4: Token metadata of the faucet.
233pub fn create_basic_fungible_faucet(
234    init_seed: [u8; 32],
235    symbol: TokenSymbol,
236    decimals: u8,
237    max_supply: Felt,
238    account_storage_mode: AccountStorageMode,
239    auth_scheme: AuthScheme,
240) -> Result<Account, FungibleFaucetError> {
241    let distribute_proc_root = BasicFungibleFaucet::distribute_digest();
242
243    let auth_component: AccountComponent = match auth_scheme {
244        AuthScheme::RpoFalcon512 { pub_key } => AuthRpoFalcon512Acl::new(
245            pub_key,
246            AuthRpoFalcon512AclConfig::new()
247                .with_auth_trigger_procedures(vec![distribute_proc_root])
248                .with_allow_unauthorized_input_notes(true),
249        )
250        .map_err(FungibleFaucetError::AccountError)?
251        .into(),
252        AuthScheme::EcdsaK256Keccak { pub_key } => AuthEcdsaK256KeccakAcl::new(
253            pub_key,
254            AuthEcdsaK256KeccakAclConfig::new()
255                .with_auth_trigger_procedures(vec![distribute_proc_root])
256                .with_allow_unauthorized_input_notes(true),
257        )
258        .map_err(FungibleFaucetError::AccountError)?
259        .into(),
260        AuthScheme::NoAuth => {
261            return Err(FungibleFaucetError::UnsupportedAuthScheme(
262                "basic fungible faucets cannot be created with NoAuth authentication scheme".into(),
263            ));
264        },
265        AuthScheme::RpoFalcon512Multisig { threshold: _, pub_keys: _ } => {
266            return Err(FungibleFaucetError::UnsupportedAuthScheme(
267                "basic fungible faucets do not support multisig authentication".into(),
268            ));
269        },
270        AuthScheme::Unknown => {
271            return Err(FungibleFaucetError::UnsupportedAuthScheme(
272                "basic fungible faucets cannot be created with Unknown authentication scheme"
273                    .into(),
274            ));
275        },
276        AuthScheme::EcdsaK256KeccakMultisig { threshold: _, pub_keys: _ } => {
277            return Err(FungibleFaucetError::UnsupportedAuthScheme(
278                "basic fungible faucets do not support EcdsaK256KeccakMultisig authentication"
279                    .into(),
280            ));
281        },
282    };
283
284    let account = AccountBuilder::new(init_seed)
285        .account_type(AccountType::FungibleFaucet)
286        .storage_mode(account_storage_mode)
287        .with_auth_component(auth_component)
288        .with_component(BasicFungibleFaucet::new(symbol, decimals, max_supply)?)
289        .build()
290        .map_err(FungibleFaucetError::AccountError)?;
291
292    Ok(account)
293}
294
295// TESTS
296// ================================================================================================
297
298#[cfg(test)]
299mod tests {
300    use assert_matches::assert_matches;
301    use miden_objects::account::auth::PublicKeyCommitment;
302    use miden_objects::{FieldElement, ONE, Word};
303
304    use super::{
305        AccountBuilder,
306        AccountStorageMode,
307        AccountType,
308        AuthScheme,
309        BasicFungibleFaucet,
310        Felt,
311        FungibleFaucetError,
312        TokenSymbol,
313        create_basic_fungible_faucet,
314    };
315    use crate::account::auth::AuthRpoFalcon512;
316    use crate::account::wallets::BasicWallet;
317
318    #[test]
319    fn faucet_contract_creation() {
320        let pub_key_word = Word::new([ONE; 4]);
321        let auth_scheme: AuthScheme = AuthScheme::RpoFalcon512 { pub_key: pub_key_word.into() };
322
323        // we need to use an initial seed to create the wallet account
324        let init_seed: [u8; 32] = [
325            90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85,
326            183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16,
327        ];
328
329        let max_supply = Felt::new(123);
330        let token_symbol_string = "POL";
331        let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap();
332        let decimals = 2u8;
333        let storage_mode = AccountStorageMode::Private;
334
335        let faucet_account = create_basic_fungible_faucet(
336            init_seed,
337            token_symbol,
338            decimals,
339            max_supply,
340            storage_mode,
341            auth_scheme,
342        )
343        .unwrap();
344
345        // The reserved faucet slot should be initialized to an empty word.
346        assert_eq!(faucet_account.storage().get_item(0).unwrap(), Word::empty());
347
348        // The falcon auth component is added first so its assigned storage slot for the public key
349        // will be 1.
350        assert_eq!(faucet_account.storage().get_item(1).unwrap(), pub_key_word);
351
352        // Slot 2 stores [num_tracked_procs, allow_unauthorized_output_notes,
353        // allow_unauthorized_input_notes, 0]. With 1 tracked procedure (distribute),
354        // allow_unauthorized_output_notes=false, and allow_unauthorized_input_notes=true,
355        // this should be [1, 0, 1, 0].
356        assert_eq!(
357            faucet_account.storage().get_item(2).unwrap(),
358            [Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO].into()
359        );
360
361        // The procedure root map in slot 3 should contain the distribute procedure root.
362        let distribute_root = BasicFungibleFaucet::distribute_digest();
363        assert_eq!(
364            faucet_account
365                .storage()
366                .get_map_item(3, [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO].into())
367                .unwrap(),
368            distribute_root
369        );
370
371        // Check that faucet metadata was initialized to the given values. The faucet component is
372        // added second, so its assigned storage slot for the metadata will be 2.
373        assert_eq!(
374            faucet_account.storage().get_item(4).unwrap(),
375            [Felt::new(123), Felt::new(2), token_symbol.into(), Felt::ZERO].into()
376        );
377
378        assert!(faucet_account.is_faucet());
379
380        assert_eq!(faucet_account.account_type(), AccountType::FungibleFaucet);
381
382        // Verify the faucet can be extracted and has correct metadata
383        let faucet_component = BasicFungibleFaucet::try_from(faucet_account.clone()).unwrap();
384        assert_eq!(faucet_component.symbol(), token_symbol);
385        assert_eq!(faucet_component.decimals(), decimals);
386        assert_eq!(faucet_component.max_supply(), max_supply);
387    }
388
389    #[test]
390    fn faucet_create_from_account() {
391        // prepare the test data
392        let mock_word = Word::from([0, 1, 2, 3u32]);
393        let mock_public_key = PublicKeyCommitment::from(mock_word);
394        let mock_seed = mock_word.as_bytes();
395
396        // valid account
397        let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol");
398        let faucet_account = AccountBuilder::new(mock_seed)
399            .account_type(AccountType::FungibleFaucet)
400            .with_component(
401                BasicFungibleFaucet::new(token_symbol, 10, Felt::new(100))
402                    .expect("failed to create a fungible faucet component"),
403            )
404            .with_auth_component(AuthRpoFalcon512::new(mock_public_key))
405            .build_existing()
406            .expect("failed to create wallet account");
407
408        let basic_ff = BasicFungibleFaucet::try_from(faucet_account)
409            .expect("basic fungible faucet creation failed");
410        assert_eq!(basic_ff.symbol(), token_symbol);
411        assert_eq!(basic_ff.decimals(), 10);
412        assert_eq!(basic_ff.max_supply(), Felt::new(100));
413
414        // invalid account: basic fungible faucet component is missing
415        let invalid_faucet_account = AccountBuilder::new(mock_seed)
416            .account_type(AccountType::FungibleFaucet)
417            .with_auth_component(AuthRpoFalcon512::new(mock_public_key))
418            // we need to add some other component so the builder doesn't fail
419            .with_component(BasicWallet)
420            .build_existing()
421            .expect("failed to create wallet account");
422
423        let err = BasicFungibleFaucet::try_from(invalid_faucet_account)
424            .err()
425            .expect("basic fungible faucet creation should fail");
426        assert_matches!(err, FungibleFaucetError::NoAvailableInterface);
427    }
428
429    /// Check that the obtaining of the basic fungible faucet procedure digests does not panic.
430    #[test]
431    fn get_faucet_procedures() {
432        let _distribute_digest = BasicFungibleFaucet::distribute_digest();
433        let _burn_digest = BasicFungibleFaucet::burn_digest();
434    }
435}