Context
GlobeWallet::add_asset, contracts/globe-wallet/src/lib.rs. Issue #29 made asset-code duplicate detection case-insensitive (codes_match_case_insensitive) so "USDC" and "usdc" can't both be registered. The native-XLM check was never updated to match.
Problem
pub fn add_asset(env: Env, user: Address, asset: AssetInfo) -> Result<(), WalletError> {
...
let is_native = asset.code == String::from_str(&env, "XLM"); // exact, case-SENSITIVE
if is_native {
if asset.issuer.is_some() { return Err(WalletError::InvalidAssetInfo); }
} else {
if asset.issuer.is_none() { return Err(WalletError::InvalidAssetInfo); } // requires an issuer
}
...
for i in 0..assets.len() {
if Self::codes_match_case_insensitive(&assets.get(i).unwrap().code, &asset.code) { // case-INSENSITIVE
return Err(WalletError::AssetAlreadyAdded);
}
}
...
}
Two different equality functions, two different canonicalizations, in the same function, checking related things.
Walk through registering "xlm" (lowercase) with a real-looking issuer before the user has ever registered real native "XLM":
is_native — exact match against "XLM" fails (it's "xlm"), so this is not treated as native.
- Falls to the
else branch: asset.issuer.is_none() is false (an issuer was supplied) — passes. A non-native, issuer-bearing asset with code "xlm" is now registered.
- The user (or their client, populating a field from a deep link / QR code / SEP-7 payload — the mobile app added exactly this kind of untrusted-input asset-add flow recently) later tries to register the real native
"XLM" (no issuer).
- The duplicate-detection loop runs
codes_match_case_insensitive("xlm", "XLM") → true (case-insensitive) → rejected as AssetAlreadyAdded.
The fake, issuer-bearing "xlm" entry permanently occupies the case-insensitive namespace slot the real native asset needs, and the real native XLM registration is blocked for good (nothing removes the fake entry automatically, and the user has no reason to suspect their own prior "xlm" entry is why "XLM" won't register).
Reproduction steps
#[test]
fn test_lowercase_xlm_with_issuer_blocks_real_native_xlm() {
let (env, _cid, admin, client) = setup();
let user = Address::generate(&env);
let fake_issuer = Address::generate(&env);
// Registers successfully -- is_native's exact match against "XLM" fails
// for "xlm", so it's treated as a normal issued asset.
let fake_xlm = AssetInfo { code: String::from_str(&env, "xlm"), issuer: Some(fake_issuer) };
client.add_asset(&user, &fake_xlm);
// The user now tries to add the real, native XLM.
let real_xlm = AssetInfo { code: String::from_str(&env, "XLM"), issuer: None };
let result = client.try_add_asset(&user, &real_xlm);
assert_eq!(result, Err(Ok(WalletError::AssetAlreadyAdded))); // blocked, permanently, by the fake entry
}
Impact
A malicious "add this asset" link (plausible via the mobile app's SEP-7/QR-driven add-asset flows) can pre-squat a case-variant of "XLM" with an attacker-controlled issuer before the user ever registers native XLM, permanently denying that user the ability to register/track their actual native balance through this contract's registry — a targeted griefing vector, and a confusing one to diagnose since nothing about the error (AssetAlreadyAdded) hints that the blocker is a case-variant, not literally "XLM" itself.
Suggested fix
Use the same case-insensitive comparison for the native-asset check that duplicate-detection already uses: Self::codes_match_case_insensitive(&asset.code, &String::from_str(&env, "XLM")). Once that's consistent, decide (and document) whether non-native assets with a case-variant of "XLM" should be rejected outright regardless of issuer, since they're indistinguishable from an attempt to impersonate the native asset.
Definition of done
Context
GlobeWallet::add_asset,contracts/globe-wallet/src/lib.rs. Issue #29 made asset-code duplicate detection case-insensitive (codes_match_case_insensitive) so "USDC" and "usdc" can't both be registered. The native-XLM check was never updated to match.Problem
Two different equality functions, two different canonicalizations, in the same function, checking related things.
Walk through registering
"xlm"(lowercase) with a real-looking issuer before the user has ever registered real native"XLM":is_native— exact match against"XLM"fails (it's"xlm"), so this is not treated as native.elsebranch:asset.issuer.is_none()is false (an issuer was supplied) — passes. A non-native, issuer-bearing asset with code"xlm"is now registered."XLM"(no issuer).codes_match_case_insensitive("xlm", "XLM")→true(case-insensitive) → rejected asAssetAlreadyAdded.The fake, issuer-bearing
"xlm"entry permanently occupies the case-insensitive namespace slot the real native asset needs, and the real native XLM registration is blocked for good (nothing removes the fake entry automatically, and the user has no reason to suspect their own prior"xlm"entry is why"XLM"won't register).Reproduction steps
Impact
A malicious "add this asset" link (plausible via the mobile app's SEP-7/QR-driven add-asset flows) can pre-squat a case-variant of
"XLM"with an attacker-controlled issuer before the user ever registers native XLM, permanently denying that user the ability to register/track their actual native balance through this contract's registry — a targeted griefing vector, and a confusing one to diagnose since nothing about the error (AssetAlreadyAdded) hints that the blocker is a case-variant, not literally"XLM"itself.Suggested fix
Use the same case-insensitive comparison for the native-asset check that duplicate-detection already uses:
Self::codes_match_case_insensitive(&asset.code, &String::from_str(&env, "XLM")). Once that's consistent, decide (and document) whether non-native assets with a case-variant of"XLM"should be rejected outright regardless of issuer, since they're indistinguishable from an attempt to impersonate the native asset.Definition of done
is_nativecheck uses the same canonicalization as the duplicate-detection loop"xlm"(with any issuer) is rejected the same way an uppercase duplicate would be, before the real native XLM is ever registeredcargo test --workspaceoutput pasted