Add reserved native contract alias resolving to the native asset#2646
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a reserved built-in contract alias, native, which always resolves to the network-specific native asset (XLM) Stellar Asset Contract and cannot be created/overwritten/removed like a normal user alias. It wires this behavior through config resolution, deploy flows, alias commands, and adds unit + integration tests to validate the new semantics.
Changes:
- Add a reserved-alias mechanism (
native) with fast-fail validation for commands that would write or deploy under a reserved alias. - Make alias resolution treat
nativeas a built-in mapping to the native asset contract for the active network; keep removal able to delete a pre-existing shadowingnative.json. - Update
contract alias ls/showbehavior and add unit + soroban-test integration coverage; regenerate help docs (includesstellar xdrdrift).
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| FULL_HELP_DOCS.md | Regenerated help output; reflects updated stellar xdr usage text. |
| cmd/soroban-cli/src/config/locator.rs | Enforces reserved aliases on save; resolves native to the native asset contract; adds unit tests. |
| cmd/soroban-cli/src/config/alias.rs | Defines reserved-alias registry and validation helpers; adds unit tests. |
| cmd/soroban-cli/src/commands/contract/deploy/wasm.rs | Validates --alias early so reserved aliases fail fast. |
| cmd/soroban-cli/src/commands/contract/deploy/asset.rs | Validates --alias early so reserved aliases fail fast. |
| cmd/soroban-cli/src/commands/contract/arg_parsing.rs | Adds test ensuring JSON address arguments resolve native correctly. |
| cmd/soroban-cli/src/commands/contract/alias/remove.rs | Uses stored-only lookup so removing a shadowed reserved alias reports the removed value. |
| cmd/soroban-cli/src/commands/contract/alias/ls.rs | Always lists built-in native per known network; marks shadowed user native as disabled. |
| cmd/soroban-cli/src/commands/contract/alias/add.rs | Prevents adding/overwriting reserved aliases. |
| cmd/crates/soroban-test/tests/it/config.rs | Adds integration tests covering native resolution, reservation enforcement, and listing behavior. |
|
Keys/identities named The new short-circuit in
Repro / impact: stellar keys generate native # still succeeds on this branch
stellar contract invoke --id CMYTOKEN... -- \
transfer --from admin --to native --amount 1000000000Before: tokens go to the This is a silent behavior change for pre-existing setups: an identity named Suggested fix (two parts):
|
|
Pre-existing stored
// locator.rs
if alias == "native" {
return Ok(Some(crate::utils::contract_id_hash_from_asset(
&xdr::Asset::Native, network_passphrase,
)));
}Every command that resolves a contract alias goes through this — The SAC implements the SEP-41 token interface, so if the stored alias pointed at a token contract, calls against the wrong contract can still succeed: # before upgrade: transfers 500 units of the aliased token
# after upgrade: transfers 50 XLM, exit 0
stellar contract invoke --id native --source treasurer -- \
transfer --from treasurer --to GPLAYER... --amount 500_0000000
Suggested change: the built-in-wins precedence looks intentional (per the code comment and the if alias == "native" {
let native = crate::utils::contract_id_hash_from_asset(&xdr::Asset::Native, network_passphrase);
if let Some(stored) = self.get_stored_contract_id(alias, network_passphrase)? {
if stored != native {
return Err(Error::ShadowedReservedAlias {
alias: alias.to_owned(),
stored,
});
}
}
return Ok(Some(native));
}with a message that includes the fix:
|
|
Workspace deploys can abort mid-loop on a reserved package name, leaving a partial deployment The fail-fast check in With workspace packages stellar contract deploy --source deployer --network mainnet
# adapter: uploaded + deployed + alias saved (2 txs, fees paid)
# native: error: contract alias 'native' is reserved and cannot be added, overwritten, or removed
# token: never reachedThe result is a partial deployment: some contracts live on-chain, some not, fees spent, nonzero exit. The error is also confusing on two counts — the user never typed an alias, and the message describes alias management ("added, overwritten, or removed"), not a deploy. Retrying compounds the problem: with no explicit salt, a rerun creates a duplicate There's also no escape hatch for the workspace case: Partial deploys can happen for other reasons too (network failures, underfunded accounts), but this failure is knowable before the first transaction — which is what the comments on both new checks say the intent is ("fails fast instead of after wasted work"). The check just needs to run before the loop rather than inside it. |
|
So To be clear, using else {
if crate::config::alias::is_reserved(&self.alias) {
return Err(locator::Error::ContractAliasReserved(self.alias.to_string()).into());
}
return Err(Error::NoContract { ... });
};The reserved error flows through the existing |
|
Cleanup: reserved-alias knowledge is spread across three modules, and the validation block is copy-pasted three times Two related maintainability points, no behavior change implied by either. 1. The
The locator compares against the literal string rather than consulting One shape for consolidating: give 2. The same 3-line validation block appears three times. if let Some(alias) = &self.alias {
crate::config::alias::validate_reserved_aliases(alias)?;
}— in This ties into the partial-deployment issue above: fixing that means moving/changing where derived aliases are validated anyway, so it's a good moment to end up with one check at the point where the alias is finalized, rather than three copies that have to stay in sync (plus the fourth in |
Fixed in `36024e79`. `write_identity`/`write_key` now call `validate_reserved_aliases`, so `keys generate native` / `keys add native` fail fast. For keys already on disk, `UnresolvedScAddress::resolve` hard-errors on the reserved-alias + key collision (`ReservedAliasShadowsKey`) and asks for an explicit `C…`/`G…`, instead of warn-and-prefer-contract. Scoped to reserved names, so unrelated ambiguous aliases keep their current behavior. |
Fixed in `36024e79`, as suggested. `get_contract_id` now returns `ShadowedReservedAlias` when a stored file points somewhere other than the SAC, with a message pointing at `contract alias remove native`. `remove` still deletes the stored file, so recovery stays one command. |
Fixed in `33dcdd82`. Package-derived aliases are now validated before the deploy loop via `reserved_package_alias`, so a workspace containing a `native` crate fails before any contract is uploaded. Added a `ReservedPackageAlias` error with a deploy-appropriate message that names the package and the `--package native --alias ` escape hatch. |
Fixed in `4ae28109`. The empty branch now returns `ContractAliasReserved` for reserved names, so `remove` agrees with `ls`/`show` instead of reporting "no contract found". The `Some` branch is unchanged, so removing a real shadowed file still works. |
Done both in `0cb97c58`. Added `alias::resolve_reserved()` as the single source of truth — `get_contract_id` and `alias ls` both go through it now, no more literal `"native"` or re-implemented resolution. Collapsed `RESERVED_ALIASES` to a single `RESERVED_ALIAS` const since there is only one. Removed the redundant `run()`-level check in `wasm.rs` so validation lives at the finalized-alias point (`run_single`) plus the pre-loop derived-alias guard. One note: I kept `format!("{contract}")` in `ls.rs` rather than `.to_string()` — `Contract` has an inherent `to_string()` returning a `heapless::String`, which does not compile against the `String` field. |
Confirmed the fix works for the stored-alias case on its own — but one edge case slipped through in (_, Ok(key)) => Ok(xdr::ScAddress::Account(...)), // matches first
(Err(err @ locator::Error::ShadowedReservedAlias { .. }), _) => Err(err.into()),A user with both pre-upgrade artifacts — a stored It takes both collisions to hit, so the population is small — but it's precisely the pre-upgrade-config population these fixes target, and the resolution is silent. Fix is to reorder the arms so the shadowed error takes precedence over the key: (Err(err @ locator::Error::ShadowedReservedAlias { .. }), _) => Err(err.into()),
(_, Ok(key)) => Ok(xdr::ScAddress::Account(...)),The existing test doesn't catch this because it only writes the key; a variant that also writes |
|
Great catch — you were right, that arm was unreachable when a key also existed. Fixed in `8a3ffa2e` by moving the `ShadowedReservedAlias` arm ahead of the key arm, so the collision wins instead of silently resolving to the key's G-address. Added a regression test that writes both a `native` key and a `contract-ids/native.json` (it would have failed under the old ordering). |
What
Adds
nativeas a built-in contract alias that resolves to the native asset (XLM) Stellar Asset Contract for the current network.nativeis reserved: it cannot be created, overwritten, or removed like a normal alias, and it resolves everywhere a contract alias/address is accepted (contract invoke --id native, address arguments,alias show, etc.).contract alias lsalways lists it as(built-in), and a pre-existing usernativealias is shown as(disabled)since the built-in shadows it (it can still be removed to clean up).Why
Today you can already derive the native asset's Stellar Asset Contract address with:
But that value isn't usable as a
nativename anywhere else — users still have to copy the address around or register an alias per network. This PR essentially closes that gap in the other commands by makingnativea first-class alias that resolves consistently wherever a contract alias/address is accepted, giving a stable, network-aware name. Reserving the name prevents a user alias from silently shadowing the native asset, which would be a confusing footgun. Reservation is enforced before any build/simulation/on-chain work, sodeploy --alias=nativefails fast rather than after deploying.Known limitations
contract alias lsnow lists every known network (defaults + saved), each showing the built-innative, whereas previously it only listed networks that had stored aliases. TheFULL_HELP_DOCS.mddiff includes unrelated regeneration drift for thestellar xdrcommand.Examples
When no stored
nativefile exists, removal errors withno contract found with alias 'native'.