fix(standards): register protocol callback slots for AllowAll transfer policy#2946
Conversation
PhilippGackstatter
left a comment
There was a problem hiding this comment.
Looks good. I left a few comment/questions.
| // Register the protocol-reserved asset-callback slots whenever any transfer policy is | ||
| // configured on this manager (active or reserved alternative, including `AllowAll`). | ||
| // With the flattened policy dispatch the kernel reads these slots directly and | ||
| // dispatches via `call`, so writing the active roots (including `AllowAll`, which is a | ||
| // no-op `dropw`) wires up the dispatch path uniformly. | ||
| // | ||
| // Registering the slots whenever transfer policies are present stamps | ||
| // `AssetCallbackFlag::Enabled` on every asset minted by this faucet (see | ||
| // `protocol::faucet::create_fungible_asset`, which gates the flag on the callback slots | ||
| // being populated). Without this, a faucet that ships with `AllowAll` for transfer | ||
| // would mint callback-less assets that are permanently exempt from any policy | ||
| // installed later via `set_send_policy` / `set_receive_policy` — fragmenting the | ||
| // circulating supply into enforceable and exempt subsets and defeating | ||
| // post-deployment compliance upgrades. |
There was a problem hiding this comment.
Nit: I think it would be better if this was more concise and included less details. Somehow the formatting is also a bit off (line wrapping at 100 characters).
Register the protocol-reserved asset-callback slots whenever any transfer policy is
configured on this manager.
Registering the slots whenever transfer policies are present stamps
`AssetCallbackFlag::Enabled` on every asset minted by this faucet. Without this, a faucet
that with `AllowAll` for transfer would mint callback-less assets that are
permanently exempt from any transfer policy installed later. This would fragment the
circulating supply into enforceable and exempt asset sets.
When no transfer policy is set, the callback slots are not added meaning all minted
assets have callbacks disabled.
Reading all of this though makes me think that adding the callback flag to minted assets based on the presence of the callback slot may be too subtle. Maybe there are cases, where faucets may want to always issue callback-enabled assets even if the faucet does not yet define any or even has the callback slots.
It may be worth another look how bad it would be to require this flag as an input instead of deriving it. It might be better to be more explicit and require the enable_callbacks flag as an input to the asset creation, even if it's a bit more annoying to carry this flag through in all the places.
There was a problem hiding this comment.
Reading all of this though makes me think that adding the callback flag to minted assets based on the presence of the callback slot may be too subtle. Maybe there are cases, where faucets may want to always issue callback-enabled assets even if the faucet does not yet define any or even has the callback slots.
I think that's a really good point, in fact, I believe that faucets should always issue callback-enabled assets, but the policies uses callback such as send/receive policies needs to be either no-op (allow all) or enforce a policy. If we somehow design the changes in that way, we might even don't need for the enable_callbacks flag as an additional parameter to carry it in all the places.
There was a problem hiding this comment.
Generally agree, but the one reason why we need the callback flag in the asset is to avoid having to load the foreign account for assets that don't need callbacks. Loading the foreign account can be quite time-consuming. Doing this only to find that no callbacks are set would add inefficiency to the callback process.
| /// Regression test for "Enabling transfer callbacks after minting permanently exempts | ||
| /// earlier assets from send/receive policy enforcement". | ||
| /// | ||
| /// A faucet deployed with only `TransferAllowAll` (no reserved alternative) must still | ||
| /// register the protocol-reserved asset-callback slots, populated with `TransferAllowAll`'s | ||
| /// procedure root. The protocol stamps `AssetCallbackFlag::Enabled` on every minted asset | ||
| /// whenever those slots are populated — without this, a later policy switch could not | ||
| /// retroactively apply to assets minted under `AllowAll`. | ||
| /// | ||
| /// Pre-fix, `manager_storage_slots` omitted the callback slots in this configuration via | ||
| /// the `needs_callbacks` predicate (it considered `AllowAll` a "no enforcement, no | ||
| /// callbacks" case). This test pins the new contract: registering any transfer policy — | ||
| /// `AllowAll` included — populates the protocol callback slots. | ||
| #[test] | ||
| fn allow_all_transfer_policy_registers_protocol_callback_slots() { |
There was a problem hiding this comment.
Nit: I think this is a bit too much detail. Especially describing the situation before the fix makes sense in the context of the PR, but will probably not be helpful when read in a month. I'd rather link to the GH issue.
This is a repeating occurrence with AI-generated docs and we're trying to reduce this with a skill, but until that lands, might be worth manually checking this.
| // Both slots must hold the AllowAll procedure root (not zero). The protocol stamps | ||
| // the callback flag on minted assets whenever these slots are non-empty. |
There was a problem hiding this comment.
| // Both slots must hold the AllowAll procedure root (not zero). The protocol stamps | |
| // the callback flag on minted assets whenever these slots are non-empty. | |
| // Both slots must hold the AllowAll procedure root (not zero). |
| /// A manager configured without any send / receive policy (e.g. used purely for mint / | ||
| /// burn policy storage, with a custom callback component owning the protocol slots) | ||
| /// must NOT register the protocol callback slots — otherwise it would collide with the | ||
| /// custom component on the same slot names. | ||
| /// | ||
| /// This pairs with [`allow_all_transfer_policy_registers_protocol_callback_slots`]: the | ||
| /// guard "register iff a transfer policy is configured" is what keeps kernel-level | ||
| /// callback test helpers working after the fix. |
There was a problem hiding this comment.
I'm not sure the reasoning for why the component should not include the callback slots here makes sense to me. I think the primary motivation is to omit the callback slots such that minted assets have callbacks disabled and do not incur that additional check during transfers.
| /// A manager configured without any send / receive policy (e.g. used purely for mint / | |
| /// burn policy storage, with a custom callback component owning the protocol slots) | |
| /// must NOT register the protocol callback slots — otherwise it would collide with the | |
| /// custom component on the same slot names. | |
| /// | |
| /// This pairs with [`allow_all_transfer_policy_registers_protocol_callback_slots`]: the | |
| /// guard "register iff a transfer policy is configured" is what keeps kernel-level | |
| /// callback test helpers working after the fix. | |
| /// A manager configured without any send / receive policy must NOT register the | |
| /// protocol callback slots — otherwise it would always needlessly mint assets with | |
| /// callbacks enabled. |
| // Execute transaction to consume the output note with the target account | ||
| // Execute transaction to consume the output note with the target account. The faucet must | ||
| // be supplied as a foreign account so the kernel can dispatch the receive callback on it | ||
| // when the asset is added to the target account's vault. | ||
| let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; |
There was a problem hiding this comment.
Nit: I think the previous comment was sufficient.
| .vault() | ||
| .get(expected_asset.vault_key()) | ||
| .expect("vault should contain the minted asset"); | ||
| assert_eq!(stored_asset.unwrap_fungible().amount(), expected_asset.amount()); |
There was a problem hiding this comment.
This is fixed in #2939. We should use get_balance here, and so it would be good not to introduce this temporary change that we'll have to revert later (in #2939 or later). So for now, I'd revert all changes to tests that have this fix.
Edit: An alternative would be to introduce a get_balance_enabled API that we'll temporarily use, as it would be easier to revert/remove than the changes here.
| let mint_asset: Asset = | ||
| FungibleAsset::new(faucet.id(), amount.as_canonical_u64()).unwrap().into(); | ||
| let mint_asset: Asset = FungibleAsset::new(faucet.id(), amount.as_canonical_u64()) | ||
| .unwrap() | ||
| .with_callbacks(AssetCallbackFlag::Enabled) | ||
| .into(); | ||
| let serial_num = Word::from([1, 2, 3, 4u32]); |
There was a problem hiding this comment.
What was the rationale for changing a couple of tests to use callback-enabled assets? For future PRs, could you include such details in the PR description as it speeds up understanding and reviews? Thanks!
PhilippGackstatter
left a comment
There was a problem hiding this comment.
Looks good to me!
| /// Returns the balance of the callback-enabled fungible asset issued by the specified | ||
| /// faucet. | ||
| /// | ||
| /// # Errors | ||
| /// Returns an error if the specified ID is not an ID of a fungible asset faucet. | ||
| pub fn get_balance_enabled( | ||
| &self, | ||
| faucet_id: AccountId, | ||
| ) -> Result<AssetAmount, AssetVaultError> { | ||
| if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) { |
There was a problem hiding this comment.
Sorry for the back and forth on this, but now that #2939 was merged, we can remove this and use get_balance directly.
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I mostly reviewed non-test code, and also tried to update the branch to the latest next - but seems like some tests are not passing now. I suspect this has something to do with AggLayer faucets always implying callbacks disabled (which should be fixed with #2963) - but also, I didn't look deep enough to know for sure.
cc @partylikeits1983 @mmagician in case you guys have some comments.
|
Seems like one test is still failing still. @onurinanc is there a path to fixing it? |
No these are not AggLayer tests. *the callbacks were not configured automatically until this PR fixed in fix: tests should respect the faucets callback flag |
| movdn.2 push.1 | ||
| # => [1, faucet_id_suffix, faucet_id_prefix, native_amount, faucet_id_suffix, faucet_id_prefix] |
There was a problem hiding this comment.
@onurinanc we should have kept this TODO, regardless of which direction is the default. We need to be able to support the registration of both types of agglayer faucets, with & without callbacks
|
|
||
| # prepare stack for asset::create_fungible_asset: [enable_callbacks, suffix, prefix, amount] | ||
| # TODO(#2963): callbacks bit is hardcoded to 1 to match AggLayer faucets configured via | ||
| # the M-01 fix (which register the protocol callback slots through their `AllowAll` |
There was a problem hiding this comment.
as per my previous comments, I'd avoid putting PR-specific changes in the doc comments. This won't make sense to anyone reading this comment outside of that PR context
Closes: #2945