feat(soroban): register the policy in the compliant token constructor - #68
Merged
Merged
Conversation
Deploying a compliant token took three transactions: uploadContractWasm, createCustomContract, then register_policy. Soroban permits one operation per transaction, so each was a separate transaction and signature — and a token that skipped the third was inert, since `transfer` validates against the registry's mapping rather than the token's own stored policy_id. Have the constructor register too, so deployment and registration land in one operation. Combined with skipping the upload when the WASM is already published, a repeat deploy becomes a single transaction. `register_policy()` is unchanged and still exported: tokens deployed before this need it, and it re-registers if the registry entry is ever cleared. Both paths now share a private `write_policy_to_registry`. The constructor deliberately does not call `admin.require_auth()`. The admin is frequently not the deployer, and requiring its signature would make those deploys impossible in one transaction. Only the token's own authorization is needed — the registry keys the policy by its caller, and Soroban grants a contract auth for itself as the invoking contract. Registering a brand-new contract's own policy affects nothing else. Tests: the constructor alone registers; it works with an admin that never signs and no mocked auths; register_policy remains idempotent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Problem
Deploying
example-compliant-tokentook three transactions:uploadContractWasm,createCustomContract, thenregister_policy. Soroban permits one operation per transaction — verified against testnet, which rejects multi-op transactions with"Transaction contains more than one operation"— so each is its own transaction and its own wallet signature.Worse, a token that skipped the third step was inert but looked fine.
registry()andpolicy_id()read back correctly, because the constructor stores them locally, whiletransfervalidates against the registry's mapping — a different contract's storage that the constructor never wrote to. This bit a real deploy: the token reportedpolicy_id = x-managed-policy-…whileregistry.get_policy_id(token)returned"".Change
The constructor registers as well, so deployment and registration land in one operation. Combined with skipping the upload when that WASM is already published (the companion change in
contractor-app), a repeat deploy becomes a single transaction.register_policy()is unchanged and still exported — tokens deployed before this need it, and it re-registers if the registry entry is ever cleared. Both paths now share a privatewrite_policy_to_registry.Why the constructor does not require admin auth
Deliberate. The admin is frequently not the deployer, and requiring its signature would make those deploys impossible in a single transaction — defeating the purpose.
It is also unnecessary.
policy::setrequires auth from its caller, and the caller here is the token itself; Soroban grants a contract authorization for its own address as the invoking contract.admin.require_auth()inregister_policyis a policy decision about who may re-register later, not what makes the write safe. And registering a brand-new contract's own policy affects nothing but that contract — it cannot touch another token's entry.register_policy()keeps its admin check, so nothing about the post-deployment path is loosened.Testing
cargo test -p example-compliant-token— 6 passed, 0 failed (3 new, 3 existing):test_constructor_registers_policy""for a contract it has never seen, so the assertion isn't vacuoustest_constructor_does_not_require_admin_authmock_all_auths(), and an admin that is not the deployer and never signs — anyrequire_authon the deploy path fails this testtest_register_policy_is_idempotentAlso
cargo build --target wasm32-unknown-unknown --releasesucceeds, andstellar contract info interfaceconfirms both__constructorandregister_policyare still exported.cargo clippy --all-targetsreports one warning, pre-existing inpredicate-client(too many arguments (9/7)), untouched here.Note:
cargo test -p predicate-registrycurrently fails to compile onmain—contractimport!atpredicate-registry/src/lib.rs:197needs a pre-built.wasmthat isn't present. I confirmed this is pre-existing by reproducing it with my change stashed; it is unrelated and left alone.🤖 Generated with Claude Code