Skip to content

feat: tansu-DAO-gated registry manager contract#5

Open
willemneal wants to merge 16 commits into
mainfrom
move/registry-tansu-manager
Open

feat: tansu-DAO-gated registry manager contract#5
willemneal wants to merge 16 commits into
mainfrom
move/registry-tansu-manager

Conversation

@willemneal
Copy link
Copy Markdown
Collaborator

@willemneal willemneal commented Jun 2, 2026

Moves the Tansu-DAO-gated registry manager contract into this repo, where the on-chain registry contracts live. History-preserving move of stellar-scaffold/cli#518 (its 13 commits are replayed here unchanged), plus one follow-up commit wiring it into this workspace.

What this adds

  • contracts/registry-tansu-manager/ — a manager contract for the on-chain registry, gated by a Tansu DAO. Its single entry point, trigger(proposal_id), reads an approved proposal from the configured Tansu project, pre-authorizes the proposal's single outcome via env.authorize_as_current_contract, then calls Tansu.execute — so the outcome (e.g. registry.publish_hash / registry.deploy) lands in one transaction, satisfying the registry's manager.require_auth() without any non-root auth recording.
  • contracts/test/tansu-stub/ — a Tansu stand-in that matches Tansu's wire format (get_proposal + a execute that auto-invokes the outcome and enforces the ProposalActive replay guard), letting the flow be exercised without live Tansu.

Follow-up wiring (one commit on top of the moved history)

  • Added contracts/registry-tansu-manager to the workspace members; the stub is already covered by contracts/test/*.
  • just build now uses stellar scaffold build so wasm is staged to target/stellar/local/, which both the registry tests' contractimport! and the manager's import_contract_client!(tansu_stub) resolve against. setup binstalls stellar-scaffold-cli.
  • Dropped the hello example payload (not moved). The e2e scripts now publish/deploy the registry contract's own wasm: e2e-testnet.sh deploys a subregistry through the proposal (exercising the registry's 3-arg __constructor) and verifies with a read-only manager() call; e2e-fast/e2e-real publish the registry wasm under the name registry.
  • Minor clippy fixes in the stub for this repo's stricter pedantic ruleset.

Verification

  • just build — all 6 contract wasms build and stage to target/stellar/local/.
  • just test — 45 tests pass (incl. the manager's constructor_stores_values).
  • cargo clippy --all (+ --tests --all-features) clean under the repo's -Dclippy::pedantic -Dwarnings.
  • cargo fmt --all -- --check clean.
  • CI green on both workflows (rust + Tests). CI now also installs stellar-scaffold (needed by just build) and nextest in tests.yml.

Live testnet e2e ✅

Ran e2e-fast-tansu-testnet.sh against testnet with the registry wasm as the swapped payload — full DAO-gated flow passed end-to-end against the custom testnet Tansu:

  • Registered a Tansu project, deployed a registry + manager, installed the manager, handed Tansu maintainership to it.
  • Created proposal (Add registry@0.1.0), voted Approve, waited out the voting period + execute delay.
  • manager.trigger drove Tansu.execute → registry.publish_hash in a single tx (confirmed by proposal_executed/publish events; wasm_name: "registry").
  • Verified registry resolved registry@0.1.0 -> f8b77e06…baa7050, and the replay guard rejected the second trigger with ProposalActive.
  • trigger tx on stellar.expert · registry CB6MC7MA…WYVE · manager CCQ7TC7F…WMNSB

Original PR (left open for context): stellar-scaffold/cli#518

willemneal and others added 16 commits May 29, 2026 16:41
New `registry-tansu-manager` contract that wraps a Tansu workspace as
the registry's manager. `execute(proposal_id)` fetches the proposal,
verifies it is `Approved` in the configured `project_key`, and forwards
its single `OutcomeContract` to the registry via XCC — satisfying the
registry's `manager.require_auth()` through contract-auth chaining.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address review feedback before opening PR:

- Add replay protection: successful `execute` records
  `DataKey::Executed(id)` in persistent storage; later calls return
  `AlreadyExecuted`. Adds a test that re-running the same proposal fails.
- Add an explicit test for outcomes targeting the manager itself
  (already blocked by `OutcomeTargetMismatch`; the test makes the
  intention durable against future refactors).
- Doc comments on every `Error` variant.
- Doc comment on `execute` covering the auth model (why no explicit
  `authorize_as_current_contract` is needed) and the project_key trust
  assumption.
- Rename `Cfg` -> `DataKey` to match the convention used by the test
  stubs and the wider Soroban ecosystem.
- Note on the Tansu-types comment that field order must stay in lock
  step with upstream `Consulting-Manao/tansu`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the hand-rolled `DataKey` enum + raw `env.storage().instance()`
calls with `#[contractstorage(auto_shorten = true)]` from
`soroban-sdk-tools`. The macro generates static one-liner accessors
(`Storage::get_tansu`, `Storage::set_executed`, `Storage::has_executed`,
…) keyed by auto-shortened symbols, eliminating the `DataKey` enum and
the persistent/instance bucket plumbing in `execute`. Behavior, ABI, and
tests are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the pieces needed to exercise the registry-tansu-manager flow against
a real network:

- `contracts/hello/`: minimal hello-world contract (admin in constructor,
  `hello(to) -> to`) used as the published+deployed-via-DAO payload.
- `contracts/test/tansu-stub/`: stand-in for Tansu's `get_proposal`,
  plus `set_deploy_proposal` and `set_proposal_outcome` helpers so a
  script can plant an `Approved` proposal directly. Avoids Tansu's
  collateral/membership/24h voting cycle for E2E. Proposal types are
  duplicated rather than path-dep'd to keep manager exports out of the
  stub's wasm.
- `contracts/registry-tansu-manager/e2e-testnet.sh`: nine-step bash
  script that on testnet (or any configured stellar network) deploys a
  fresh registry, publishes hello, deploys stub + manager, installs the
  manager, plants a deploy-proposal, executes via the manager, invokes
  the deployed hello, and verifies replay rejection. Verified green
  against testnet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI runs `just clippy` which adds `-Dclippy::pedantic` without allowing
`needless_pass_by_value`, unlike the workspace .cargo/config.toml.

- Switch entry-point signatures (`__constructor`, accessors, `execute`,
  and the hello contract's methods) to take `&Env` / `&Address` /
  `&Bytes`, matching the existing pattern in `contracts/registry` and
  `contracts/test/hello_world`.
- Add `#![allow(clippy::needless_pass_by_value, clippy::should_panic_without_expect)]`
  to `registry-tansu-manager/src/test.rs` (stub contracts in the test
  module deliberately keep wide value signatures).
- Same allow on `contracts/test/tansu-stub/src/lib.rs` — the stub mirrors
  Tansu's owned-value calling convention and isn't worth the noise of
  reworking each helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com>
…tub) and AuthClient in tests

Drops the manager's hand-copied Tansu types in favor of a typed `Client` and
`Proposal`/`OutcomeContract`/`ProposalStatus` derived from the stub's wasm
spec. The tansu-stub crate is now the single source of truth for those types
(still hand-mirrored from upstream Consulting-Manao/tansu — collision in
their real wasm spec blocks importing it directly: see
Consulting-Manao/tansu#152).

The registry forward call (`env.invoke_contract(&registry, &oc.execute_fn,
oc.args)`) stays untyped on purpose: an approved proposal targets any
registry method, so a typed client can't express the arbitrary-method
forward.

Test-side changes:
- Extract the inline RegistryStub to its own `contracts/test/registry-stub`
  crate so it can be wasm-imported.
- Wasm-import the stub via `soroban_sdk_tools::contractimport!` so tests get
  the `AuthClient` builder, replacing the prior `setup_mock_auth(...)` +
  hand-built MockAuth in `registry_rejects_direct_caller`.
- Drop the inline TansuStub from `test.rs`; use the standalone tansu-stub
  crate (already used by the testnet e2e script) instead.
- Add a generic `set_proposal(project_key, proposal)` helper to tansu-stub
  so unit tests can plant non-Approved statuses and `None`-outcome shapes.

Build order is handled by topo-sorting on Cargo edges: tansu-stub is in the
manager's `[dependencies]` (build-only signal — the cdylib never links), and
registry-stub is in `[dev-dependencies]` (used only in tests). Pattern
mirrors `contracts/registry`'s existing dev-dep on `hello_world`.

Verification: `just build` clean; `cargo test -p registry-tansu-manager`
9/9 pass; `cargo clippy ... -- -D warnings` clean; `cargo fmt --check` clean.

Addresses PR #518 review threads on lib.rs:165 and tansu-stub/lib.rs:13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The existing `e2e-testnet.sh` runs against the tansu-stub. This new script
exercises the same flow against the **live** Tansu DAO on testnet
(`CBXKUSLQ…NHGZA`) — the real one chadoh kept asking for, with collateral,
voting, and the 24h period. Two phases because `MIN_VOTING_PERIOD` is
hardcoded:

  ./e2e-real-tansu-testnet.sh setup
      - generates maintainer + voter testnet accounts (auto-funded)
      - registers a fresh Tansu project (auto-registers the SorobanDomain
        name on .xlm under the maintainer)
      - adds maintainer + voter as Tansu members
      - uploads hello.wasm; deploys a fresh registry (G-account admin/manager);
        deploys registry-tansu-manager (tansu = live Tansu)
      - registry.set_manager(manager_contract) — DAO now gates publishing
      - creates a Tansu proposal whose outcome targets
        `registry.publish_hash(hello, author, wasm_hash, "0.1.0")`
      - voter casts Approve (proposer is auto-Abstained by Tansu, hence the
        two-account model); state saved to a sidecar env file

  ./e2e-real-tansu-testnet.sh finalize [state-file]   # ≥ 24h later
      - Tansu.execute (Active -> Approved, refunds proposal collateral)
      - manager.execute -> registry.publish_hash via XCC
      - asserts registry.fetch_hash returns the wasm hash we uploaded
      - replay-guards a second manager.execute

Phase 1 verified end-to-end on testnet today; phase 2 will be runnable
2026-05-29T13:50:35Z onward.

Constraints learned while building this:
  * Tansu project names: ≤15 chars and `[a-z]+` only (SorobanDomain rule)
  * Proposal collateral: 70_000_000 stroops (7 XLM); vote collateral
    20_000_000 stroops (2 XLM) — both refunded on execute
  * Tansu auto-adds the proposer to the Abstain group, so single-account
    runs would deadlock at the vote step
  * SorobanDomain validates names too; we self-register it via Tansu's
    `register` (which calls `domain_register` if the node isn't taken)

State files are gitignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ore manager

`stellar scaffold build` orders contract compilation by walking Cargo edges
where the dep has `[package.metadata.stellar] contract = true`. Without that
flag on tansu-stub, the manager (which `import_contract_client!`s
tansu_stub.wasm in non-test code) could be built before the stub.

Locally this happened to work because `target/stellar/local/tansu_stub.wasm`
was lying around from earlier builds; CI's clean checkout had no such
fallback and the macro then tried `stellar registry download tansu_stub`,
which is not a subcommand the CI's stellar-cli has.

Verified by removing `target/stellar/local/tansu_stub.wasm` and
`registry_tansu_manager.wasm` then running `just build` end-to-end clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l amounts

Header reflow: the Tansu testnet contract now carries its explorer link,
and the collateral note distinguishes PROPOSAL_COLLATERAL (7 XLM, paid at
create_proposal) from VOTE_COLLATERAL (2 XLM, paid per vote) — the earlier
"~11 XLM" was a single rough sum. Both are refunded on Tansu.execute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Saves a working state where the manager has typed no-op proxy methods
(`publish_hash`, `manager_only`) that Tansu's auto-invocation lands on,
and `execute(proposal_id)` re-reads the proposal and forwards
`oc.execute_fn + oc.args` to the registry with the manager's auth.

Fast e2e against custom Tansu (CDK7JBII...XJ26UON) passes end-to-end in
~2.5min. Unit tests 9/9 green.

About to refactor to custom-account `__check_auth` design which collapses
this to a single tx and ties auth to the Tansu.execute call chain
cryptographically. Tagging here in case we need to revert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ze_as_current_contract

Replaces the no-op-proxy + separate manager.execute pattern with a single
`trigger(proposal_id)` entry point.

Flow:
  1. trigger reads the proposal from the configured Tansu under the
     configured project_key — wrong-project callers can't piggyback.
  2. Pre-authorizes *this contract's auth* for exactly the proposal's
     single approved-branch outcome via env.authorize_as_current_contract.
     Nothing else gets authorized.
  3. Calls Tansu.execute(self, project_key, proposal_id, _, _). Tansu
     tallies, flips to Approved, auto-invokes the outcome (e.g.
     registry.publish_hash). The pre-authorization satisfies the
     registry's manager.require_auth — publish runs in the same tx.

Deployment requirement: the manager must be a Tansu project maintainer
(set via Tansu::register or update_config). That makes the manager the
direct caller of Tansu.execute, so Tansu's internal
maintainer.require_auth is satisfied by contract-implicit auth — no
auth entry needed, no recording-mode non-root auth issue.

Why this over a custom-account `__check_auth`: stellar-cli 26.0.0 does
not expose `enable_non_root_authorization`, so a pure __check_auth design
fails simulation in recording-auth mode (deep require_auth not tied to
the root invocation). authorize_as_current_contract is the production
soroban-sdk primitive for this — same security guarantee (manager only
authorizes publishes for proposals from its own DAO), works with the
current CLI and frontend SDKs without extra plumbing.

Storage::executed (replay guard) removed — Tansu's own
`if proposal.status != Active { panic }` inside execute prevents the
same proposal being driven twice, so the on-manager replay map was
redundant.

Tests: replaced the inline TansuStub/RegistryStub unit tests with a
constructor smoke test. The contracts/test/registry-stub crate is
removed (no callers). The integration behavior is covered by
contracts/registry-tansu-manager/e2e-fast-tansu-testnet.sh on testnet.

E2E updates:
- Both e2e scripts now include a Tansu.update_config call to hand
  maintainership to the manager contract after deploy.
- Both finalize/run phases call manager.trigger(proposal_id) and
  verify the publish landed via registry.fetch_hash.
- Replay check runs trigger a second time and asserts Tansu's
  ProposalActive (#402).

Verification: e2e-fast-tansu-testnet.sh against custom Tansu
CDK7JBII...XJ26UON ran end-to-end in ~2.5min; replay rejected.
Single Tansu.execute tx (~50M stroops gas) drives the whole flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stub gains a Tansu-shaped `execute(maintainer, project_key, proposal_id,
tallies, seeds) -> ProposalStatus` plus `Error::ProposalActive = 402`
(matches Tansu's #402). It auto-invokes the proposal's index-0 outcome
the same way real Tansu does, and sets an `Executed(project_key,
proposal_id)` storage marker so a second call panics with the same
#402 callers would see against live Tansu.

This lets the stub-based smoke loop exercise the new `manager.trigger`
path end-to-end without needing live testnet Tansu:

  manager.trigger(id)
   ├── reads proposal from stub.get_proposal under our project_key
   ├── env.authorize_as_current_contract(outcome)
   └── stub.execute(self, project_key, id, _, _)
         └── env.invoke_contract(outcome) -> registry.deploy(...)
               └── manager.require_auth -> matched by pre-auth
                     -> hello deploys

Verified by running e2e-testnet.sh against testnet — `hello(world)` returns
"world" on the freshly deployed contract; second trigger rejects with #402.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…orkspace

Target-specific follow-up after replaying PR stellar-scaffold/cli#518's
history into this repo. Pure additions on top of the moved 13 commits:

- Cargo.toml: add contracts/registry-tansu-manager to workspace members
  (tansu-stub is already matched by contracts/test/*).
- registry-tansu-manager/Cargo.toml: inherit repository from [workspace.package]
  instead of pointing at the scaffold-stellar tree.
- .gitignore: ignore e2e-real-tansu-state-*.env sidecar files.
- justfile: build via `stellar scaffold build` (not plain `stellar contract
  build`) so wasm is staged to target/stellar/<network>/, which the registry
  tests' `contractimport!` and the manager's `import_contract_client!(tansu_stub)`
  both resolve against. Pin STELLAR_NETWORK=local to match the
  target/stellar/local/ paths the tests import from, and binstall
  stellar-scaffold-cli in `setup`.
- e2e scripts: drop the `hello` example payload (not moved); publish/deploy the
  registry contract's own wasm instead. e2e-testnet now deploys a subregistry
  through the proposal (registry's 3-arg __constructor) and verifies via a
  read-only manager() call; tansu-stub's set_deploy_proposal builds that
  constructor shape. e2e-fast/e2e-real publish the registry wasm under the name
  `registry`.
- tansu-stub: satisfy this repo's stricter pedantic clippy (doc backticks,
  used_underscore_binding allow for macro-generated dispatch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`just build` now invokes `stellar scaffold build`, which needs the
`stellar-scaffold` plugin on PATH — CI only installed `stellar-cli`. Binstall
`stellar-scaffold-cli@0.0.24` (prebuilt, no compile) in both rust.yml and
tests.yml after the stellar-cli step.

Also add `taiki-e/install-action@nextest` to tests.yml: `just test` runs
`cargo t` (= `nextest run` per .cargo/config.toml) but tests.yml never
installed nextest, so the job had been failing with exit 101 on main too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…om tag

Replace the opaque epoch-digit→a-j mapping (which produced names like
`ffbhiaedcehb`) with a readable prefix plus a random lowercase suffix:
`fast<rand>` / `real<rand>` (8 random [a-z], 12 chars total). Stays within
Tansu's SorobanDomain constraint (≤15 chars, lowercase [a-z] only — no digits,
so the prefix can't be `e2e*`), and the random tag avoids "name taken"
collisions on re-runs. e2e-real still persists the chosen name to its state
file so `finalize` reuses it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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