Skip to content

feat(soroban): register the policy in the compliant token constructor - #68

Merged
alex-predicate merged 1 commit into
mainfrom
alex/constructor-registers-policy
Aug 6, 2026
Merged

feat(soroban): register the policy in the compliant token constructor#68
alex-predicate merged 1 commit into
mainfrom
alex/constructor-registers-policy

Conversation

@alex-predicate

Copy link
Copy Markdown
Contributor

Problem

Deploying example-compliant-token took three transactions: uploadContractWasm, createCustomContract, then register_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() and policy_id() read back correctly, because the constructor stores them locally, while transfer validates against the registry's mapping — a different contract's storage that the constructor never wrote to. This bit a real deploy: the token reported policy_id = x-managed-policy-… while registry.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 private write_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::set requires 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() in register_policy is 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 What it pins down
test_constructor_registers_policy Deployment alone registers; also asserts the registry returns "" for a contract it has never seen, so the assertion isn't vacuous
test_constructor_does_not_require_admin_auth No mock_all_auths(), and an admin that is not the deployer and never signs — any require_auth on the deploy path fails this test
test_register_policy_is_idempotent The old entry point still works and re-registering is harmless

Also cargo build --target wasm32-unknown-unknown --release succeeds, and stellar contract info interface confirms both __constructor and register_policy are still exported.

cargo clippy --all-targets reports one warning, pre-existing in predicate-client (too many arguments (9/7)), untouched here.

Note: cargo test -p predicate-registry currently fails to compile on maincontractimport! at predicate-registry/src/lib.rs:197 needs a pre-built .wasm that 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

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>

@douglasmakey douglasmakey left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@alex-predicate
alex-predicate merged commit da17a32 into main Aug 6, 2026
4 checks passed
@alex-predicate
alex-predicate deleted the alex/constructor-registers-policy branch August 6, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants