Skip to content

[RWF] asset-hub-rococo: NonTransfer proxy allows ForeignAssets and PoolAssets transfers #12221

@xlc

Description

@xlc

Found by the runtime whitebox fuzzer.

Summary

asset-hub-rococo documents ProxyType::NonTransfer as a proxy that can execute calls that do not transfer funds or assets. Its filter rejects Balances, Assets, NftFractionalization, Nfts, and Uniques, but it omits ForeignAssets and PoolAssets.

Both omitted variants are separate pallet_assets instances with transfer dispatchables. A NonTransfer proxy can therefore pass ForeignAssets::transfer and PoolAssets::transfer calls.

Severity and sensitivity

Severity: medium. Sensitivity: non-sensitive runtime configuration defect suitable for a public issue. Impact is access-control expectation mismatch: an account granting a NonTransfer proxy would not expect that proxy to move foreign or pool assets.

Source evidence

  • NonTransfer docs:
    scale_info::TypeInfo,
    )]
    pub enum ProxyType {
    /// Fully permissioned proxy. Can execute any call on behalf of _proxied_.
    Any,
    /// Can execute any call that does not transfer funds or assets.
    NonTransfer,
  • Filter rejects some transfer-capable variants but omits ForeignAssets and PoolAssets:
    impl InstanceFilter<RuntimeCall> for ProxyType {
    fn filter(&self, c: &RuntimeCall) -> bool {
    match self {
    ProxyType::Any => true,
    ProxyType::NonTransfer => !matches!(
    c,
    RuntimeCall::Balances { .. } |
    RuntimeCall::Assets { .. } |
    RuntimeCall::NftFractionalization { .. } |
    RuntimeCall::Nfts { .. } |
    RuntimeCall::Uniques { .. }
    ),
  • Assets, ForeignAssets, and PoolAssets are distinct runtime call variants:
    Assets: pallet_assets::<Instance1> = 50,
    Uniques: pallet_uniques = 51,
    Nfts: pallet_nfts = 52,
    ForeignAssets: pallet_assets::<Instance2> = 53,
    NftFractionalization: pallet_nft_fractionalization = 54,
    PoolAssets: pallet_assets::<Instance3> = 55,

Reproduction

cargo test -p asset-hub-rococo-runtime tests::generated_tests::test_risk_c8uxvpea_non_transfer_rejects_foreign_assets_transfer -- --ignored --exact --nocapture
cargo test -p asset-hub-rococo-runtime tests::generated_tests::test_risk_c8uxvpea_non_transfer_rejects_pool_assets_transfer -- --ignored --exact --nocapture

Observed failures:

ProxyType::NonTransfer must reject transfer-capable call: 'ForeignAssets::transfer'
ProxyType::NonTransfer must reject transfer-capable call: 'PoolAssets::transfer'

Focused PoC:

use frame_support::traits::InstanceFilter;

#[ignore]
#[test]
fn test_risk_c8uxvpea_non_transfer_rejects_foreign_assets_transfer() {
    let call = RuntimeCall::ForeignAssets(pallet_assets::Call::<Runtime, ForeignAssetsInstance>::transfer {
        id: xcm::v5::Location::here(),
        target: sp_runtime::MultiAddress::Id(AccountId::new([0u8; 32])),
        amount: 0u128,
    });
    assert!(!ProxyType::NonTransfer.filter(&call),
        "ProxyType::NonTransfer must reject transfer-capable call: 'ForeignAssets::transfer'");
}

#[ignore]
#[test]
fn test_risk_c8uxvpea_non_transfer_rejects_pool_assets_transfer() {
    let call = RuntimeCall::PoolAssets(pallet_assets::Call::<Runtime, PoolAssetsInstance>::transfer {
        id: 0u32,
        target: sp_runtime::MultiAddress::Id(AccountId::new([0u8; 32])),
        amount: 0u128,
    });
    assert!(!ProxyType::NonTransfer.filter(&call),
        "ProxyType::NonTransfer must reject transfer-capable call: 'PoolAssets::transfer'");
}

Expected behavior

ProxyType::NonTransfer should reject RuntimeCall::ForeignAssets { .. } and RuntimeCall::PoolAssets { .. } transfer-capable calls, consistent with its documented non-transfer contract.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions