Skip to main content

miden_standards/account/faucets/
basic_fungible.rs

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