Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9611f93
feat(agglayer): configure priced fee policies at deployment
partylikeits1983 Aug 4, 2026
059ec00
docs: document AggLayer constant fee policies
partylikeits1983 Aug 4, 2026
bd884d8
refactor(agglayer): add fee policy account builders
partylikeits1983 Aug 4, 2026
cd44ca2
Merge remote-tracking branch 'origin/next' into ajl-agglayer-constant…
partylikeits1983 Aug 4, 2026
ba4b1b7
refactor(agglayer): streamline fee policy builders
partylikeits1983 Aug 4, 2026
6a67dd0
test(agglayer): clarify fee manager names
partylikeits1983 Aug 4, 2026
96561ba
refactor(agglayer): simplify testing helper imports
partylikeits1983 Aug 4, 2026
0f51cac
test(agglayer): cover bridge flows with fees enabled
partylikeits1983 Aug 4, 2026
9be17fe
refactor(tests): unify AggLayer fee policy setup
partylikeits1983 Aug 4, 2026
bb76288
Merge branch 'next' into ajl-agglayer-constant-fee-policy
partylikeits1983 Aug 5, 2026
0814b44
fix: address fee policy deployment review
partylikeits1983 Aug 5, 2026
1d729e2
feat(agglayer): make deployed fee schedules repricable
partylikeits1983 Aug 5, 2026
0fd1985
docs(agglayer): specify fee schedule administration
partylikeits1983 Aug 5, 2026
b787c15
Merge remote-tracking branch 'origin/next' into ajl-agglayer-constant…
partylikeits1983 Aug 6, 2026
490df44
docs(agglayer): reword the SPEC term flagged by the spellchecker
partylikeits1983 Aug 6, 2026
0e1b272
chore: condense the AggLayer fee policy changelog entries
partylikeits1983 Aug 6, 2026
f821485
refactor(tests): deduplicate AggLayer fee-enabled test plumbing
partylikeits1983 Aug 6, 2026
8b05fec
Merge branch 'next' into ajl-agglayer-constant-fee-policy
partylikeits1983 Aug 6, 2026
a7fb9ac
Update CHANGELOG.md
partylikeits1983 Aug 7, 2026
c235c26
Merge remote-tracking branch 'origin/next' into ajl-agglayer-constant…
partylikeits1983 Aug 7, 2026
ef12824
refactor(agglayer): return AccountBuilder from the account builder he…
partylikeits1983 Aug 7, 2026
a524eb8
refactor(agglayer): rename the note-set accessors
partylikeits1983 Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@

### Features

- [BREAKING] AggLayer bridge and faucet accounts now take their fee policy manager at deployment, built by the new `NetworkNotePricer::agglayer_bridge_fee_policy_manager` / `agglayer_faucet_fee_policy_manager` ([#3486](https://github.com/0xMiden/protocol/pull/3486)).
- [BREAKING] AggLayer bridge and faucet accounts install the `ConstantFeeManager` so their `ADMIN` role can reprice the deployed fee schedule; the faucet is now `Authority::RbacControlled` and its constructors take a deployment admin ([#3486](https://github.com/0xMiden/protocol/pull/3486)).

### Changes

- [BREAKING] Renamed `AggLayerBridge::allowed_notes` / `AggLayerFaucet::allowed_notes` to `bridge_notes` / `faucet_notes`; `allowed_notes` now returns the full set of note script roots the deployed account accepts (the account-specific roots plus the `AuthNetworkAccount` defaults), which its fee policy must price ([#3486](https://github.com/0xMiden/protocol/pull/3486)).
- Documented the RBAC freeze-only actor pattern on `Authority` and added test coverage pinning that a `FREEZER` can trip the emergency switch but can never unfreeze the account ([#3520](https://github.com/0xMiden/protocol/pull/3520)).

### Fixes
Expand Down
7 changes: 5 additions & 2 deletions bin/bench-transaction/src/context_setups/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ use std::collections::BTreeSet;

use anyhow::Result;
pub use miden_agglayer::testing::ClaimDataSource;
use miden_agglayer::testing::{bridge_admin_account_id, create_existing_bridge_account_with_roles};
use miden_agglayer::testing::{
bridge_admin_account_id,
create_existing_agglayer_faucet,
create_existing_bridge_account_with_roles,
};
use miden_agglayer::{
AggLayerBridge,
B2AggNote,
Expand All @@ -14,7 +18,6 @@ use miden_agglayer::{
MetadataHash,
RemoveGerNote,
UpdateGerNote,
create_existing_agglayer_faucet,
};
use miden_protocol::account::auth::AuthScheme;
use miden_protocol::account::{Account, StorageMapKey};
Expand Down
98 changes: 90 additions & 8 deletions crates/miden-agglayer/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ implementation are called out inline with `TODO (Future)` markers.
| **AggLayer Faucet** | Fungible faucet that represents a single bridged token. Mints on bridge-in claims, burns on bridge-out. Each foreign token has its own faucet instance. | `FungibleFaucet`, network-mode, with `agglayer_faucet` component |
| **Integration Service** (offchain) | Observes L1 events (deposits, GER updates) and creates UPDATE_GER and CLAIM notes on Miden. Trusted to provide correct proofs and data. | Not an onchain entity; creates notes targeting bridge/faucet |
| **Bridge Operator** (offchain) | Deploys bridge and faucet accounts. Creates CONFIG_AGG_BRIDGE notes to register faucets. Must hold the `FAUCET_MNGR` role. | Not an onchain entity; creates config notes |
| **Role Admin** (offchain) | Holds the bridge's `ADMIN` role and manages role membership via RBAC_CONFIG notes. Root authority: effective admin of every operational role unless delegated, so compromise of this key is equivalent to compromise of all operational roles (see [Section 2.5](#25-administration)). | Not an onchain entity; creates RBAC_CONFIG notes |
| **Role Admin** (offchain) | Holds the `ADMIN` role on the bridge and on each faucet. Manages bridge role membership via RBAC_CONFIG notes and reprices both accounts' fee schedules via CONSTANT_FEE_POLICY_CONFIG notes. Root authority: effective admin of every operational role unless delegated, and on a faucet it can additionally retarget the `Ownable2Step` owner, so compromise of this key is equivalent to compromise of all operational roles (see [Section 2.5](#25-administration)). | Not an onchain entity; creates RBAC_CONFIG and CONSTANT_FEE_POLICY_CONFIG notes |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this fit in with #2724?

The idea was that the faucet does not have ownership transfer.


---

Expand Down Expand Up @@ -255,7 +255,8 @@ so while the bridge is paused it rejects all bridge-out, claim, GER-injection, a
faucet-management operations. `remove_ger` is deliberately exempt: a paused bridge can still
revoke a fraudulent GER, and because `update_ger` is paused the revoked GER cannot be
re-injected until unpause. The management notes (`RBAC_CONFIG`, `NETWORK_ACCOUNT_CONFIG`,
`PAUSE_CONFIG`) remain consumable while paused, so a paused bridge can still be administered.
`PAUSE_CONFIG`, `CONSTANT_FEE_POLICY_CONFIG`) remain consumable while paused, so a paused bridge
can still be administered.

The pause is toggled via the standards [`PAUSE_CONFIG`](#411-pause_config-standards) note, which
dispatches to `PausableManager`'s `pause` / `unpause`. These have no entry in the bridge's
Expand All @@ -265,6 +266,39 @@ The pause complements the `Authority` freeze switch: freezing blocks every autho
procedure - including `remove_ger` - but not `claim` / `bridge_out`, while the pause is the
inverse.

#### Fee schedule administration

Both network accounts are deployed with a `BasicConstantFeePolicy` whose schedule prices every
note they can consume from that note's benchmarked consumption cost under the chain's
`FeeParameters` (`NetworkNotePricer::agglayer_bridge_fee_policy_manager` /
`agglayer_faucet_fee_policy_manager`). A schedule entry is a bare amount denominated in the
chain's fee asset, so it is only correct for the verification base fee it was priced under.

Because that base fee is a chain parameter which moves independently of these accounts, both
install the `miden-standards` `ConstantFeeManager` and allowlist the
[`CONSTANT_FEE_POLICY_CONFIG`](#412-constant_fee_policy_config-standards) note, which dispatches
to its `set_note_fee`. Neither account maps that procedure in its `Authority` procedure-to-role
map, so authorization falls back to the `ADMIN` role, exactly as for the pause toggles. Without
it a schedule would be frozen at its deployment prices for the account's whole lifetime.

Repricing the faucet matters as much as repricing the bridge. When the bridge creates a `MINT` or
`BURN` note it sizes that note's fee sponsorship by reading the *faucet's* schedule as a foreign
account, so a faucet left at stale prices makes the bridge under-sponsor the note it has just
created, and the faucet's transaction can no longer pay its own fee. A frozen faucet schedule
therefore stalls bridging, not merely faucet administration.

The config note's own script root is deliberately scheduled at zero on both accounts, even though
it has a benchmarked cost like any other note. Consuming one is the only route to `set_note_fee`,
so a priced entry could raise the config note's own fee past what a sponsor covers and put the
schedule permanently out of reach.
Comment on lines +290 to +293

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order for the network account to be able to consume the config note, the bridge still has to pay a fee somehow.

And if we price the config note at zero, then the owner that sends a config note to the bridge has no way of estimating how much they should sponsor.

So I think we need to also price-in the config notes


That reach is not hypothetical: a priced schedule requires every consumed note's fee to be
prepaid. A note whose schedule entry is non-zero can only be consumed together with
`FEE_SPONSORSHIP` notes bound to it that cover that entry, independently of the chain's own
verification base fee. Operators therefore fund a sponsorship alongside each management note
(`RBAC_CONFIG`, `NETWORK_ACCOUNT_CONFIG`, `PAUSE_CONFIG`), and repricing is the one
administrative action that never needs one.

---

## 3. Contracts and Public Interfaces
Expand Down Expand Up @@ -454,7 +488,8 @@ access-control components at account creation time.
### 3.2 Faucet Account Component

The faucet account has the `agglayer_faucet` component (`components/faucet.masm`),
which is a thin wrapper, on top of `Ownable2Step` + `OwnerControlled`, that re-exports the
which is a thin wrapper, on top of `Ownable2Step` + the RBAC access-control stack
(`RoleBasedAccessControl` + `Authority::RbacControlled`), that re-exports the
standard Miden fungible-faucet procedures:

- `mint_and_send` (from `miden::standards::faucets::fungible::mint_and_send`)
Expand Down Expand Up @@ -501,12 +536,25 @@ This is a re-export of `miden::standards::faucets::fungible::receive_and_burn`.
|-----------|-----------|----------------|---------|
| Faucet metadata (standard) | Value | `[token_supply, max_supply, decimals, token_symbol]` | Standard `NetworkFungibleFaucet` metadata |

**Companion component storage slots:** The faucet account also includes storage from
companion components required by `fungible::mint_and_send`:
**Companion component storage slots:** The faucet account also includes storage from its
companion components - those required by `fungible::mint_and_send`, plus the access-control and
fee-administration stack:

- `Ownable2Step` owner config slot: stores the bridge account ID as owner.
- `OwnerControlled` slots (3): `active_policy_proc_root`, `allowed_policy_proc_roots`,
`policy_authority`.
- `TokenPolicyManager` slots: `active_policy_proc_root`, `allowed_policy_proc_roots` per policy
kind.
- `RoleBasedAccessControl` role storage, seeded with the deployment `ADMIN` member, and the
`Authority` config slot (`RbacControlled`) with an empty procedure-to-role map.
- `ConstantFeeManager` value slot: the ID of the fee schedule slot it reprices.

**Access control:** minting and burning are gated on the `Ownable2Step` owner (the bridge), which
`MintOwnerOnly` / `BurnOwnerOnly` assert directly via `ownable2step::assert_sender_is_owner`
rather than through `Authority`. Everything else on the faucet - its fee schedule, its mint / burn
/ transfer policies and its metadata - is authority-gated and so resolves to the `ADMIN` role
through the unmapped-procedure fallback. The two authorities are therefore separate: `ADMIN`
administers the faucet's configuration without being able to mint, and the bridge mints without
being able to reconfigure. Note that `ADMIN` can retarget the `Ownable2Step` owner, so it must be
held by a strongly authenticated account (see [Section 1](#1-entities-and-trust-model)).

---

Expand Down Expand Up @@ -1072,7 +1120,7 @@ note via `output_note::add_asset`.
**Purpose:** Toggles the bridge's emergency pause (see [Section 2.5](#25-administration)). This is
the `miden-standards` `PAUSE_CONFIG` note (`pause_config.masm` / `PauseConfigNote`), not an
agglayer-specific note; the bridge merely includes its script root in
[`AggLayerBridge::allowed_notes`]. Its single storage felt is a selector: `0` dispatches to
[`AggLayerBridge::bridge_notes`]. Its single storage felt is a selector: `0` dispatches to
`PausableManager::pause`, `1` to `PausableManager::unpause`.

**Consumption:** The script loads the selector and `call`s the matching `PausableManager`
Expand All @@ -1092,6 +1140,40 @@ with clearer error reporting for non-public targets.

---

### 4.12 CONSTANT_FEE_POLICY_CONFIG (standards)

**Purpose:** Reprices one entry of a network account's `BasicConstantFeePolicy` fee schedule
after deployment (see [Section 2.5](#25-administration)). This is the `miden-standards`
`CONSTANT_FEE_POLICY_CONFIG` note (`constant_fee_policy_config.masm` /
`ConstantFeePolicyConfigNote`), not an agglayer-specific note; both the bridge and the faucet
include its script root in their allowlists. Its storage is
`[NOTE_SCRIPT_ROOT, FEE_ASSET_ID, FEE_ASSET_VALUE]`: the schedule key to rewrite, and the fee to
schedule for it.

**Consumption:** The script asserts the consuming account matches its `NetworkAccountTarget`
attachment, then `call`s `ConstantFeeManager::set_note_fee`, which runs
`authority::assert_authorized` before writing the schedule entry. The supplied asset's ID must
match the account's configured fee asset ID. On both AggLayer accounts `set_note_fee` has no
mapped role, so the note sender must hold the `ADMIN` role.

`set_note_fee` carries no `pausable::assert_not_paused`, so a paused bridge can still be
repriced, consistent with the other management notes.

Because the note is allowlisted and scheduled free, anyone can author one targeting either
account; unauthorized or misdirected notes abort at the target and authorization checks with no
state change, but since the transaction aborts no nullifier is produced and such notes remain as
permanently-unconsumable entries that may require operator-side filtering. This is inherent to
every allowlisted network-note root, not specific to this note.

#### Permissions

| Role | Enforcement |
|------|------------|
| **Issuer** | Holders of the `ADMIN` role only -- **enforced** by `ConstantFeeManager::set_note_fee` via `authority::assert_authorized` (unmapped-procedure fallback) |
| **Consumer** | Bridge or faucet account -- **enforced**: the script asserts the consuming account matches the `NetworkAccountTarget` attachment |

---

## 5. Amount Conversion

*This section is a placeholder. Content to be added.*
Expand Down
30 changes: 22 additions & 8 deletions crates/miden-agglayer/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ use miden_protocol_build_utils::{
generate_error_file,
};
use miden_standards::StandardsLib;
use miden_standards::account::access::{AccessControl, Authority, Pausable, PausableManager};
use miden_standards::account::access::{AccessControl, Pausable, PausableManager};
use miden_standards::account::auth::AuthNetworkAccount;
use miden_standards::account::fees::{BasicConstantFeePolicy, FeePolicyManager};
use miden_standards::account::fees::{
BasicConstantFeePolicy,
ConstantFeeManager,
FeePolicyManager,
};
use miden_standards::account::policies::{
BurnPolicy,
MintPolicy,
Expand Down Expand Up @@ -188,12 +192,12 @@ fn generate_agglayer_constants(
AccountComponent::new(Arc::unwrap_or_clone(package), vec![], dummy_metadata.clone())
.unwrap();

// The faucet account includes Ownable2Step and OwnerControlled components for mint and burn
// policies alongside the agglayer faucet component, since
// fungible::mint_and_send requires these for access control.
// The faucet account includes an Ownable2Step component alongside the agglayer faucet
// component, since fungible::mint_and_send gates minting on the owner, plus the RBAC
// access-control stack that gates everything else.
//
// Use a dummy owner for commitment computation - the actual owner is set at runtime. Only
// the component code (not storage) contributes to the code commitment.
// Use a dummy owner and admin for commitment computation - the real ones are set at
// runtime. Only the component code (not storage) contributes to the code commitment.
let dummy_owner = miden_protocol::account::AccountId::try_from(
miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
)
Expand Down Expand Up @@ -231,11 +235,19 @@ fn generate_agglayer_constants(
});
components.push(AccountComponent::from(Pausable::unpaused()));
components.push(AccountComponent::from(PausableManager));
components
.push(AccountComponent::from(ConstantFeeManager::for_basic_constant_fee_policy()));
} else if component_name == "faucet" {
components.push(AccountComponent::from(
miden_standards::account::access::Ownable2Step::new(dummy_owner),
));
components.push(AccountComponent::from(Authority::OwnerControlled));
// The faucet installs the same RBAC access-control stack as the bridge, so its
// ADMIN role can reprice the fee schedule and administer its policies. Minting and
// burning remain gated on the Ownable2Step owner, which the policies check directly.
components.extend(AccessControl::Rbac {
admin: dummy_owner,
procedure_roles: std::collections::BTreeMap::new(),
});
// Mirror the component order used by `create_agglayer_faucet_builder` in lib.rs so
// the compile-time code commitment matches the one computed at runtime.
//
Expand All @@ -251,6 +263,8 @@ fn generate_agglayer_constants(
.build();

components.extend(token_policy_manager);
components
.push(AccountComponent::from(ConstantFeeManager::for_basic_constant_fee_policy()));
}

// use `AccountCode` to merge codes of agglayer and authentication components
Expand Down
36 changes: 28 additions & 8 deletions crates/miden-agglayer/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ use miden_protocol::note::{Note, NoteScriptRoot};
#[cfg(any(feature = "testing", test))]
use miden_standards::account::access::PausableStorage;
use miden_standards::account::access::RoleConfig;
use miden_standards::account::auth::AuthNetworkAccount;
use miden_standards::note::{
ConstantFeePolicyConfigNote,
NetworkAccountTarget,
NetworkAccountTargetError,
NoteExecutionHint,
Expand Down Expand Up @@ -470,20 +472,21 @@ impl AggLayerBridge {
// ALLOWED SCRIPTS
// --------------------------------------------------------------------------------------------

/// Returns the set of input-note script roots that AggLayer bridge accounts accept.
/// Returns the bridge-specific input-note script roots.
///
/// The bridge's [`AuthNetworkAccount`] component is initialized with this allowlist, which
/// means any transaction consuming a note outside this set is rejected before reaching
/// `output_note::create`.
/// The bridge's [`AuthNetworkAccount`] component is initialized with this set as its explicit
/// allowlist; [`Self::allowed_notes`] is the resulting full set of notes the account accepts.
///
/// Besides the agglayer-specific notes, the bridge accepts two standards notes: the
/// [`PauseConfigNote`], so the `ADMIN` role can toggle the emergency pause, and the
/// Besides the agglayer-specific notes, the set contains three standards notes: the
/// [`PauseConfigNote`], so the `ADMIN` role can toggle the emergency pause, the
/// role-management [`RbacConfigNote`], which makes the bridge's RBAC role graph mutable
/// on-chain (see the [`RbacConfigNote`] security considerations and the Administration
/// section of `SPEC.md` for the associated caveats).
/// section of `SPEC.md` for the associated caveats), and the
/// [`ConstantFeePolicyConfigNote`], so the `ADMIN` role can reprice the bridge's fee
/// schedule after deployment.
///
/// [`AuthNetworkAccount`]: miden_standards::account::auth::AuthNetworkAccount
pub fn allowed_notes() -> BTreeSet<NoteScriptRoot> {
pub fn bridge_notes() -> BTreeSet<NoteScriptRoot> {
BTreeSet::from([
ClaimNote::script_root(),
B2AggNote::script_root(),
Expand All @@ -493,9 +496,26 @@ impl AggLayerBridge {
RemoveGerNote::script_root(),
PauseConfigNote::script_root(),
RbacConfigNote::script_root(),
ConstantFeePolicyConfigNote::script_root(),
])
}

/// Returns every input-note script root a newly deployed AggLayer bridge account accepts.
///
/// This is [`Self::bridge_notes`] plus the configuration and sponsorship notes that
/// [`AuthNetworkAccount`] adds to every standard network account. Any transaction consuming a
/// note outside the account's allowlist is rejected before reaching `output_note::create`,
/// and the bridge's deployed fee policy must schedule a fee for every root in it.
///
/// This is the deployment-time allowlist: an `ADMIN`-authored `NETWORK_ACCOUNT_CONFIG` note
/// can add or remove entries later, so read a live account's allowlist via
/// [`NetworkAccount::allowed_notes`](miden_standards::account::auth::NetworkAccount::allowed_notes).
pub fn allowed_notes() -> BTreeSet<NoteScriptRoot> {
let mut notes = Self::bridge_notes();
notes.extend(AuthNetworkAccount::default_allowed_note_scripts());
notes
}

Comment on lines +513 to +518

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why exactly do we need to differentiate between bridge_notes and allowed_notes? Couldn't we just extend the set of allowed notes with AuthNetworkAccount::default_allowed_note_scripts()?

// PAUSE NOTE
// --------------------------------------------------------------------------------------------

Expand Down
Loading
Loading