Skip to main content

miden_agglayer/
lib.rs

1#![no_std]
2
3extern crate alloc;
4
5use miden_core::Word;
6use miden_protocol::account::{AccountBuilder, AccountId};
7use miden_protocol::assembly::Path;
8use miden_protocol::asset::{AssetAmount, TokenSymbol};
9use miden_protocol::note::NoteScript;
10use miden_protocol::vm::Package;
11use miden_standards::account::access::{
12    Authority,
13    Ownable2Step,
14    Pausable,
15    PausableManager,
16    RoleBasedAccessControl,
17    RoleConfig,
18};
19use miden_standards::account::auth::NetworkAccount;
20use miden_standards::account::faucets::{FungibleFaucet, TokenName};
21use miden_standards::account::fees::{
22    BasicConstantFeePolicy,
23    ConstantFeeManager,
24    FeePolicyManager,
25};
26use miden_standards::account::policies::{BurnPolicy, MintPolicy, TokenPolicyManager};
27use miden_utils_sync::LazyLock;
28
29pub mod agglayer_note;
30pub mod b2agg_note;
31pub mod bridge;
32pub mod claim_note;
33pub mod config_note;
34pub mod costs;
35pub mod deregister_note;
36pub mod errors;
37pub mod eth_types;
38pub mod faucet;
39mod ger_note;
40pub mod remove_ger_note;
41#[cfg(any(feature = "testing", test))]
42pub mod testing;
43pub mod update_ger_note;
44pub mod utils;
45
46pub use agglayer_note::AgglayerNote;
47pub use b2agg_note::B2AggNote;
48pub use bridge::{AggLayerBridge, AgglayerBridgeError, BridgeRoles, RemovedGerHashChain};
49pub use claim_note::{
50    CgiChainHash,
51    ClaimNote,
52    ClaimNoteStorage,
53    ExitRoot,
54    LeafData,
55    LeafValue,
56    ProofData,
57    SmtNode,
58};
59pub use config_note::{ConfigAggBridgeNote, ConversionMetadata};
60pub use deregister_note::DeregisterAggFaucetNote;
61#[cfg(any(test, feature = "testing"))]
62pub use eth_types::GlobalIndexExt;
63pub use eth_types::{GlobalIndex, GlobalIndexError, MetadataHash};
64pub use faucet::AggLayerFaucet;
65pub use remove_ger_note::RemoveGerNote;
66pub use update_ger_note::UpdateGerNote;
67pub use utils::Keccak256Output;
68
69// AGGLAYER ACCOUNT COMPONENTS
70// ================================================================================================
71
72static AGGLAYER_PACKAGE: LazyLock<Package> = LazyLock::new(|| {
73    let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/miden-agglayer.masp"));
74    Package::read_from_bytes_trusted(bytes).expect("shipped AggLayer package is well-formed")
75});
76
77static BRIDGE_COMPONENT_PACKAGE: LazyLock<Package> = LazyLock::new(|| {
78    let bytes =
79        include_bytes!(concat!(env!("OUT_DIR"), "/assets/components/miden-agglayer-bridge.masp"));
80    Package::read_from_bytes_trusted(bytes)
81        .expect("shipped bridge component package is well-formed")
82});
83
84/// Returns the AggLayer package containing all agglayer modules, including the note scripts.
85///
86/// The note scripts this crate builds are external references into this package rather than
87/// self-contained copies of it, so it must be registered with the MAST store of any executor that
88/// runs AggLayer notes. This mirrors the standard note scripts, which are external references into
89/// the standards library. `TransactionMastStore::new` preloads both packages, so the in-repo
90/// prover and test executors resolve AggLayer notes automatically; a downstream executor that
91/// supplies its own `DataStore` must register this package into it (e.g. via
92/// `TransactionMastStore::insert_package`), exactly as it must already register the standards
93/// package to run standard notes.
94pub fn agglayer_package() -> Package {
95    AGGLAYER_PACKAGE.clone()
96}
97
98/// Resolves the note script exported at `path` from the AggLayer package.
99///
100/// `path` must be the fully qualified path of a procedure carrying the `@note_script` attribute,
101/// e.g. `::agglayer::notes::claim::main`.
102pub(crate) fn note_script(path: &str) -> NoteScript {
103    NoteScript::from_package_reference(&AGGLAYER_PACKAGE, Path::new(path))
104        .expect("agglayer package contains the note script procedure")
105}
106
107/// Returns the Bridge component package.
108fn agglayer_bridge_component_package() -> Package {
109    BRIDGE_COMPONENT_PACKAGE.clone()
110}
111
112// AGGLAYER ACCOUNT CREATION HELPERS
113// ================================================================================================
114
115impl AggLayerBridge {
116    /// Returns an [`AccountBuilder`] for a bridge account with the standard configuration.
117    ///
118    /// `bridge_admin` is the initial member of the bridge's built-in `ADMIN` role. `fee_policy`
119    /// must contain entries for [`AggLayerBridge::allowed_notes`], denominated in the asset issued
120    /// by `fee_faucet_id`.
121    pub fn account_builder(
122        seed: Word,
123        bridge_admin: AccountId,
124        roles: BridgeRoles,
125        network_id: u32,
126        fee_faucet_id: AccountId,
127        fee_policy: BasicConstantFeePolicy,
128    ) -> AccountBuilder {
129        let fee_policy_manager = FeePolicyManager::builder()
130            .fee_faucet_id(fee_faucet_id)
131            .active_fee_policy(fee_policy.into())
132            .build();
133        NetworkAccount::builder(seed.into(), AggLayerBridge::allowed_notes(), fee_policy_manager)
134            .expect("bridge note allowlist is non-empty")
135            .with_component(AggLayerBridge::new(network_id))
136            .with_component(
137                RoleBasedAccessControl::builder()
138                    .role(
139                        RoleConfig::new(RoleBasedAccessControl::admin_role())
140                            .with_member(bridge_admin),
141                    )
142                    .roles(roles)
143                    .build()
144                    .expect("the bridge seeds distinct non-empty roles administered by ADMIN"),
145            )
146            .with_component(Authority::RbacControlled {
147                procedure_roles: AggLayerBridge::procedure_roles(),
148            })
149            .with_component(Pausable::unpaused())
150            .with_component(PausableManager)
151            .with_component(ConstantFeeManager::for_basic_constant_fee_policy())
152    }
153}
154
155impl AggLayerFaucet {
156    /// Returns an [`AccountBuilder`] for a faucet account with the specified deployment
157    /// configuration.
158    ///
159    /// The account is a standard [`FungibleFaucet`]: `mint_and_send` and `receive_and_burn` drive
160    /// bridge-in and bridge-out, and the standard metadata getters expose the token name, symbol
161    /// and decimals. Conversion metadata (origin address, origin network, scale, metadata hash)
162    /// lives on the bridge and is written there at registration time.
163    ///
164    /// No send or receive policies are registered, so the account is created with asset callbacks
165    /// disabled.
166    ///
167    /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role; `fee_manager`
168    /// is the initial member of its `FEE_MNGR` role; `bridge_account_id` is its [`Ownable2Step`]
169    /// owner, which is what the `owner_only` mint and burn policies gate on. `fee_policy` must
170    /// contain entries for [`AggLayerFaucet::allowed_notes`], denominated in the asset issued by
171    /// `fee_faucet_id`.
172    ///
173    /// # Panics
174    ///
175    /// Panics if:
176    /// - `decimals` exceeds [`FungibleFaucet::MAX_DECIMALS`];
177    /// - `initial_supply` exceeds `max_supply`.
178    #[allow(clippy::too_many_arguments)]
179    pub fn account_builder(
180        seed: Word,
181        token_name: TokenName,
182        token_symbol: TokenSymbol,
183        decimals: u8,
184        max_supply: AssetAmount,
185        initial_supply: AssetAmount,
186        faucet_admin: AccountId,
187        fee_manager: AccountId,
188        bridge_account_id: AccountId,
189        fee_faucet_id: AccountId,
190        fee_policy: BasicConstantFeePolicy,
191    ) -> AccountBuilder {
192        let fee_policy_manager = FeePolicyManager::builder()
193            .fee_faucet_id(fee_faucet_id)
194            .active_fee_policy(fee_policy.into())
195            .build();
196        let faucet = FungibleFaucet::builder()
197            .name(token_name)
198            .symbol(token_symbol)
199            .decimals(decimals)
200            .max_supply(max_supply)
201            .token_supply(initial_supply)
202            .build()
203            .expect("agglayer faucet decimals and supplies should be within their valid ranges");
204
205        let token_policy_manager = TokenPolicyManager::builder()
206            .active_mint_policy(MintPolicy::owner_only())
207            .active_burn_policy(BurnPolicy::owner_only())
208            .build();
209
210        let rbac = RoleBasedAccessControl::builder()
211            .role(RoleConfig::new(RoleBasedAccessControl::admin_role()).with_member(faucet_admin))
212            .role(RoleConfig::new(AggLayerFaucet::fee_manager_role()).with_member(fee_manager))
213            .build()
214            .expect("the faucet seeds non-empty roles administered by ADMIN");
215
216        NetworkAccount::builder(seed.into(), AggLayerFaucet::allowed_notes(), fee_policy_manager)
217            .expect("faucet note allowlist is non-empty")
218            .with_component(faucet)
219            .with_component(Ownable2Step::new(bridge_account_id))
220            .with_component(rbac)
221            .with_component(Authority::RbacControlled {
222                procedure_roles: AggLayerFaucet::procedure_roles(),
223            })
224            .with_components(token_policy_manager)
225            .with_component(ConstantFeeManager::for_basic_constant_fee_policy())
226    }
227}
228
229// TESTS
230// ================================================================================================
231
232#[cfg(test)]
233mod tests {
234    use miden_core::Felt;
235    use miden_protocol::account::AssetCallbackFlag;
236    use miden_protocol::asset::AssetCallbacks;
237    use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE;
238    use miden_standards::tx_script::ExpirationTransactionScript;
239
240    use super::*;
241    use crate::testing::{
242        create_existing_agglayer_faucet,
243        create_existing_bridge_account_with_roles,
244    };
245
246    /// AggLayer faucets omit transfer policies and callback slots, so moving their assets skips
247    /// callback dispatch and the associated foreign-account read.
248    #[test]
249    fn agglayer_faucet_has_asset_callbacks_disabled() {
250        let id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
251
252        let faucet = create_existing_agglayer_faucet(
253            Word::default(),
254            "AggLayer Token",
255            "AGG",
256            6,
257            Felt::from(1000u32),
258            Felt::ZERO,
259            id,
260            id,
261        );
262
263        for slot_name in AssetCallbacks::slot_names() {
264            assert!(
265                faucet.storage().get(slot_name).is_none(),
266                "faucet should not install the {slot_name} callback slot"
267            );
268        }
269        assert_eq!(faucet.id().asset_callback_flag(), AssetCallbackFlag::Disabled);
270    }
271
272    /// Both agglayer network accounts allowlist the canonical [`ExpirationTransactionScript`],
273    /// which the network transaction builder attaches to every network transaction.
274    #[test]
275    fn agglayer_accounts_allowlist_expiration_tx_script() {
276        let id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
277
278        let bridge =
279            create_existing_bridge_account_with_roles(Word::default(), id, id, id, id, id, id, 77);
280        let faucet = create_existing_agglayer_faucet(
281            Word::default(),
282            "AggLayer Token",
283            "AGG",
284            6,
285            Felt::from(1000u32),
286            Felt::ZERO,
287            id,
288            id,
289        );
290
291        for account in [bridge, faucet] {
292            let network_account = NetworkAccount::try_from(account).unwrap();
293            assert!(network_account.allows_tx_script(&ExpirationTransactionScript::script_root()));
294        }
295    }
296}