Repository navigation
feat(contracts): the Veil Names registry, with an upgrade path - #1328
Merged
Merged
Conversation
Names are rented, not sold: one to five years at a time, priced per year by length — 3 USDC for 1-5 characters, 5 for 6-10, 2 for 11 and up. The middle band is the dearest on purpose; short names are scarce and long ones are nobody's first choice, so the premium sits where the demand is. A year is a flat 365 days, never a calendar year, so a renewal never turns on which February it was bought in. A name resolves to the wallet's C address. Three to twenty characters, lowercase letters, digits and underscore. No KYC anywhere in the path. Built against stellar-tokens 0.4.1 on soroban-sdk 22 deliberately: the sdk-28 line would move the WASM hashes of contracts already deployed to mainnet, and those are permanent. WIP: `cargo build --all` is clean, but `src/test.rs` does not compile. Roughly sixteen assertions expect `Err(Ok(Error::X))` while the functions raise with `panic_with_error!`, so `try_*` actually yields `Err(Err(InvokeError::Contract(code)))`. The assertions need rewriting before this crate can be merged anywhere. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
…s tests build The registry holds names people paid for, so it is the one contract that cannot be replaced by deploying a second one: a new contract starts empty and every existing name stays behind in the old one. The price bands, the recovery window and the term bounds are all product decisions, and the pricing model has already been respec'd once, so freezing them at deployment is the same trap that left three defects unfixable in the deployed wallet. Adds, all admin-gated: - `upgrade(new_wasm_hash)` — swaps the WASM. Nothing checks that the incoming build keeps the storage layout, so the doc comment says plainly that the replacement has to be built from this source. - `set_admin(new_admin)` — the mitigation path, so the role can move to a multisig or a timelock. Deliberately no way to renounce it: that would freeze the reserved-name list and the upgrade path together, which is the state this change exists to avoid. - `get_admin()` — whoever can replace the logic governing a name is something the holder should be able to read, not infer from deployment notes. The module doc now says the registry is upgradeable and that the admin is therefore a trusted party, rather than claiming the opposite. Separately: this crate's 485-line test file had never compiled. Every entrypoint panics with an error code rather than returning `Result<_, Error>`, so the error side of a `try_` call is `soroban_sdk::Error` and not this crate's enum — all 16 `Err(Ok(Error::X))` assertions were a type error. `cargo build --all` passed throughout, which is why it went unnoticed. Adds an `err()` helper that converts through `Error::from_contract_error` and rewrites the assertions against it. With the file building, the original 31 tests all pass — the contract logic was sound, it just had no running coverage. Six more cover the admin gate and the hand-over. The successful WASM swap is not unit-tested, because it needs a second contract already uploaded to the ledger and a unit test cannot upload this crate's own build output; that belongs to a testnet deploy rehearsal. `cargo build --all` clean, `cargo test` 212 passed across the workspace. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
… contract Adding `contracts/veil-names` to the workspace moved `invisible_wallet.wasm`, `multisig_wallet.wasm` and `vault.wasm` without touching a line of their source. CI proved it from both sides: the reproducible-build job is green on main at f663bf3, and the same job on this branch — f663bf3 plus one `members` entry and a new crate — computed a different hash for all three. A bare `cargo build` builds every member in one invocation, and Cargo unifies features across an invocation. `stellar-tokens` turns something on in a shared transitive dependency, and the other members get compiled with it. The lockfile diff showed no version change at all, only the two added packages, so nothing in review would have caught this. That breaks the one claim this script exists to make. The header says it lets a third party confirm a deployed hash was produced from this source — which is false if the hash also depends on which unrelated contracts happen to share the workspace. It also silently retires the review rule that more than one hash moving for a single-contract change is the signal and not a detail; that rule is how four wrong hashes were caught on #1268. So the script now builds one package at a time. With `veil-names` present, all five existing artifacts come out byte-identical to the committed hashes and only `veil_names.wasm` is new, so `expected-hashes.json` gains exactly one line and the wallet's hash is left alone. The alternative — rewriting three hashes to make the red go away — would have recorded a names-only change as having altered the wallet. Costs near nothing: 113s against ~106s, because the members share a target directory so dependencies compile once and later packages reuse them. Members are read from Cargo.toml rather than hardcoded, so the next contract is picked up without touching this script, and each member's package name is read from its own manifest because the two differ — `invisible_wallet/` holds the package `invisible-wallet`, and `-p invisible_wallet` matches nothing. veil_names.wasm = e5fa8d241d3f7d36003781a5e7832d72e32cb62276fb4780ff8711fab2f7c4e3, taken from the `got` line of this PR's own CI run and confirmed by a local build in the pinned image. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lands
contracts/veil-names— the on-chain source of truth for who holds aVeil name — plus the upgrade path it needs before it can go anywhere near
mainnet.
Contracts-only. Nothing in
frontend/changes, nothing deploys from this, anddeployments.jsonis untouched.Why it is upgradeable when the wallet is not
The registry is the one contract that cannot be replaced by deploying a second
one: a new contract starts empty and every name already registered stays
behind in the old one. The price bands, the recovery window and the term
bounds are all product decisions, and the pricing model has already been
respec'd once — so freezing them at deployment is the same trap that left
three defects permanently unfixable in the deployed wallet.
Three admin-gated entrypoints:
upgrade(new_wasm_hash)— swaps the WASM. Nothing checks that the incomingbuild preserves the storage layout, so the doc comment says plainly that the
replacement has to be built from this source rather than merely resemble it.
set_admin(new_admin)— the mitigation path, so the role can move to amultisig or a timelock. Deliberately no way to renounce it to a burn
address: that would freeze the reserved-name list and the upgrade path
together, which is the state this change exists to avoid.
get_admin()— whoever can replace the logic governing a name is somethingthe holder should be able to read, not infer from deployment notes.
The module doc previously asserted the contract could not be upgraded. It now
says it can, and that the admin is therefore a trusted party and the registry
should not be described as trustless until that role sits behind a multisig.
The test file had never compiled
Every entrypoint returns a bare value and panics via
panic_with_error!;none returns
Result<_, Error>. So the error side of atry_call issoroban_sdk::Error, not this crate's enum, and all 16Err(Ok(Error::X))assertions were a type error — the compiler says as much: "
Errorandsoroban_sdk::Errorhave similar names, but are actually distinct types."cargo build --allpassed the whole time, which is why it went unnoticed.Fixed with an
err()helper converting throughError::from_contract_error.With the file building, all 31 original tests pass — the contract logic
was sound, it simply had no running coverage. Six more cover the admin gate
and the hand-over.
The successful WASM swap is deliberately not unit-tested: it needs a second
contract already uploaded to the ledger, and a unit test cannot upload this
crate's own build output. That belongs to a testnet deploy rehearsal, and the
test file says so rather than pretending otherwise.
Expected CI failure
Adding a workspace member makes the reproducible-build check fail by
construction — the script unions
expectedandcomputed, soveil_names.wasmreports DRIFT withexpectedmissing. That job'sgotlineis the authoritative hash and the only way to obtain it; it will be committed
to
contracts/expected-hashes.jsonon this branch once CI prints it.Local computation is not an option: running the pinned image on a tree whose
contracts/is byte-identical tomainreproduced onlyfactoryandmock_walletand drifted on the other three, so no locally produced hash canbe trusted here.
Verification
cargo build --all— cleancargo test— 212 passed, 0 failed (22 + 147 + 37 + 5 + 1)https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB