Background
Protocol v0.14.0 added two new asset callback hooks:
These hooks let custom account components run MASM procedures whenever an asset is about to be added. The NetworkFungibleFaucet already implements one such callback (distribute).
The gap
The CHANGELOG and PR descriptions document what the callbacks do, but the protocol specification does not define the contract they must uphold for implementors. Four specific invariants are currently unspecified:
-
Invocation ordering when a single note adds multiple assets, are hooks fired asset-by-asset in declaration order, all-or-nothing per note, or in some other order? A callback that reads the vault to check an issuance cap needs this guarantee to reason about intermediate state correctness.
-
Abort semantics can a callback trap to veto an asset addition? If it does, does the whole transaction fail or only the current note? Is partial rollback possible?
-
State visibility can a callback observe vault mutations committed by an earlier callback in the same transaction? This is a re-entrancy-adjacent question that allows callbacks to interact non-locally, which could be exploited in multi-component accounts.
-
Recursive invocation can a callback itself call add_asset, thereby triggering recursive callback invocations? If so, what bounds recursion depth and prevents stack exhaustion?
Impact
Without these invariants, third-party components implementing these hooks including the NetworkFungibleFaucet already shipped in v0.14.0 cannot formally verify their correctness. This is a gap in the v0.14.0 security model for composable account components.
Proposed deliverable
A documentation section in the account-component spec titled "Asset Callback Guarantees" that covers:
- Invocation count: exactly once per asset addition, before the asset is written to the vault
- Ordering: sequential, in component declaration order, for each asset in turn
- Abort: a trap inside a callback propagates as
TransactionError and fails the entire transaction; no partial-note state is committed
- Re-entrancy: callbacks must not call
add_asset directly or indirectly; if they do, behavior is defined (error, panic, or explicitly permitted with bounded depth)
Background
Protocol v0.14.0 added two new asset callback hooks:
on_before_asset_added_to_account(feat: implement asset callbacks #2571)on_before_asset_added_to_note(feat: implementon_before_asset_added_to_notecallback #2595)These hooks let custom account components run MASM procedures whenever an asset is about to be added. The
NetworkFungibleFaucetalready implements one such callback (distribute).The gap
The CHANGELOG and PR descriptions document what the callbacks do, but the protocol specification does not define the contract they must uphold for implementors. Four specific invariants are currently unspecified:
Invocation ordering when a single note adds multiple assets, are hooks fired asset-by-asset in declaration order, all-or-nothing per note, or in some other order? A callback that reads the vault to check an issuance cap needs this guarantee to reason about intermediate state correctness.
Abort semantics can a callback trap to veto an asset addition? If it does, does the whole transaction fail or only the current note? Is partial rollback possible?
State visibility can a callback observe vault mutations committed by an earlier callback in the same transaction? This is a re-entrancy-adjacent question that allows callbacks to interact non-locally, which could be exploited in multi-component accounts.
Recursive invocation can a callback itself call
add_asset, thereby triggering recursive callback invocations? If so, what bounds recursion depth and prevents stack exhaustion?Impact
Without these invariants, third-party components implementing these hooks including the
NetworkFungibleFaucetalready shipped in v0.14.0 cannot formally verify their correctness. This is a gap in the v0.14.0 security model for composable account components.Proposed deliverable
A documentation section in the account-component spec titled "Asset Callback Guarantees" that covers:
TransactionErrorand fails the entire transaction; no partial-note state is committedadd_assetdirectly or indirectly; if they do, behavior is defined (error, panic, or explicitly permitted with bounded depth)