feat: tansu-DAO-gated registry manager contract#518
Conversation
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>
Seems like this should live in |
chadoh
left a comment
There was a problem hiding this comment.
Started this review from the e2e test stuff and worked my way down. Would like to do another pass at the stuff at the top. But there's enough feedback here already that I figured I'd submit now.
| echo "==> Uploading hello.wasm" | ||
| HELLO_HASH=$(stellar contract upload --wasm "$HELLO_WASM" \ | ||
| --source "$ADMIN_ID" --network "$NETWORK") | ||
| echo " hash: $HELLO_HASH" | ||
|
|
||
| echo "==> Publishing hello@$HELLO_VERSION (author=$AUTHOR_ADDR, manager=$ADMIN_ID)" | ||
| stellar contract invoke --id "$REGISTRY_ID" \ | ||
| --source "$ADMIN_ID" --network "$NETWORK" \ | ||
| -- publish_hash \ | ||
| --wasm_name hello \ | ||
| --author "$AUTHOR_ADDR" \ | ||
| --wasm_hash "$HELLO_HASH" \ | ||
| --version "$HELLO_VERSION" |
There was a problem hiding this comment.
Should these be combined to use stellar registry publish instead? Or to at least call invoke -- publish directly, instead of invoke -- publish_hash?
| TANSU_ID=$(stellar contract deploy --wasm "$STUB_WASM" \ | ||
| --source "$ADMIN_ID" --network "$NETWORK" \ | ||
| --alias "tansu-stub-${RUN_ID}") | ||
| echo " stub: $TANSU_ID" | ||
|
|
||
| # 4. Manager pointing at the stub + registry. | ||
| echo "==> Deploying registry-tansu-manager" | ||
| MANAGER_ID=$(stellar contract deploy --wasm "$MANAGER_WASM" \ | ||
| --source "$ADMIN_ID" --network "$NETWORK" \ | ||
| --alias "manager-e2e-${RUN_ID}" \ | ||
| -- \ | ||
| --tansu "$TANSU_ID" \ | ||
| --project_key "$PROJECT_KEY" \ | ||
| --registry "$REGISTRY_ID") | ||
| echo " manager: $MANAGER_ID" |
There was a problem hiding this comment.
Also strikes me as a little odd that registry tests wouldn't use registry for all of these wasm uploads & contract deploys. Like, sure, we don't need to. But we've got a nifty naming tool, why aren't we using it?
That said, if it keeps the tests simpler/more minimal/focused to skip that, I'm fine keeping as-is.
| // Duplicated here (rather than path-dep'd) because linking the manager crate as | ||
| // an `rlib` would re-export its `#[contractimpl]` functions into this stub's | ||
| // wasm. |
There was a problem hiding this comment.
Can you explain why "re-exporting its #[contractimpl] functions into this stub's wasm" is a bad thing? To me it seems possibly preferable to duplicating and "keeping in lock-step" (how are we actually planning to keep these in lock-step?)
There was a problem hiding this comment.
This was a quick and dirty way. I think I'll update to use contract import instead. Basically when "importing" another contract you don't want to use an rlib because of the way the spec generation macros work. Essentially they all get pulled in even if your contract doesn't export those types just uses them. The preferred way is via Wasm imports so that you get the real spec and not need to copy/paste. I'll fix it.
There was a problem hiding this comment.
Now using stellar_registry::import_contract_client!(tansu_stub) from the manager — the stub crate is the single source of truth for the Tansu types, and the manager derives Proposal/OutcomeContract/ProposalStatus + a typed get_proposal Client from the stub's wasm spec (no rlib involved, so no #[contractimpl] re-export). One hand-copy remains — in this stub crate — mirroring the upstream Consulting-Manao/tansu types.
We tried to drop that copy too by importing the real Tansu wasm, but it has a Contract name collision (#[contract] pub struct Contract + #[contracttype] pub struct Contract) that crashes contractimport!. Filed Consulting-Manao/tansu#152 asking for the rename; once it ships, the stub stops needing its own copy and can contractimport! the real wasm directly.
| @@ -0,0 +1,16 @@ | |||
| [package] | |||
| name = "tansu-stub" | |||
| description = "Minimal stand-in for the Tansu DAO contract. Used by the e2e script for the registry-tansu-manager flow on testnet — it lets a caller plant a synthetic `Approved` proposal so `RegistryTansuManager::execute(proposal_id)` can be exercised without running Tansu's real voting cycle (collateral, members, 24h voting period)." | |||
There was a problem hiding this comment.
I wonder if using the real tansu would be more robust. The 24hr voting period is the real dealbreaker, but perhaps we can update Tansu to make this configurable via environment variable. Then we can compile a custom Tansu and deploy it as part of the e2e script.
- Create a new project
- Add minimum number of voters
- Then we're good to go?
How complex is that?
How complex is it, compared to maintaining a long-lived stub in a separate repo that is (magically?) kept "in lock-step" with the real Tansu?
There was a problem hiding this comment.
I went back and forth on it. The tricky part with using a real contract is 1) the constructor is not simple, 2) won't require the rewrite as you say, and 3) adds a new dep.
My thoughts are that we will work on the contract itself later and it is better to just handle the current API with the stub (with one real one on testnet which shows a success with the 24 hours). Then once updated we can move it as a first class contract in the registry as we are planning and then handle the rewrite then which should be straight forward.
Since we are the maintainers of Tansu the changes won't spring up on us and we can use this as a vehicle for testing.
| "contracts/registry", | ||
| "contracts/registry-tansu-manager", | ||
| "contracts/hello", | ||
| "contracts/test/*", | ||
| "crates/stellar-scaffold-test/fixtures/contracts/*", |
There was a problem hiding this comment.
Seems weird to me that we can't list contracts/* here. It makes me wonder if there's a more natural place to put these new tests, and everything in contracts/test/*.
But also w/e ¯\_(ツ)_/¯
There was a problem hiding this comment.
The issue is that contracts/* will fail on the contracts/tests/* because that folder doesn't have a Cargo.toml. I do agree we need to move the hello to the test folder.
Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com>
chadoh
left a comment
There was a problem hiding this comment.
A couple small questions/notes, but nothing blocking!
| let proposal: Proposal = env.invoke_contract( | ||
| &tansu, | ||
| &Symbol::new(env, "get_proposal"), | ||
| vec![env, project_key.into_val(env), proposal_id.into_val(env)], | ||
| ); |
There was a problem hiding this comment.
Why are we doing cross-contract calls this manual way? Instead of using our own import_contract_client! macro? Are we awaiting the completion of import_contract! to further simplify the interface?
There was a problem hiding this comment.
Switched to stellar_registry::import_contract_client!(tansu_stub) — the manager now gets a typed Client::get_proposal and the Proposal/OutcomeContract/ProposalStatus types from the stub's wasm spec. The forward call to the registry (env.invoke_contract(®istry, &oc.execute_fn, oc.args)) stays untyped on purpose: an approved proposal targets any registry method, and a typed client can't express an arbitrary-method forward.
We can't import the real Consulting-Manao/tansu wasm yet — its v2.0.2 spec contains both #[contract] pub struct Contract and #[contracttype] pub struct Contract (a typed-Address wrapper used in set_nqg_contract etc.). contractimport! then generates a pub trait Contract colliding with the contracttype struct in the same module. experimental_spec_shaking_v2 doesn't help (the type is reachable through the trait). Opened Consulting-Manao/tansu#152 proposing the upstream rename — once it lands in a release we re-pin and drop the stub-as-source-of-truth.
import_contract! (#419) is still the right end state on top of this.
| // Pre-compute the manager address so the registry can be constructed with it | ||
| // as `manager`. (Registers a transient placeholder, then registers the real | ||
| // manager contract at a deterministic address derived from arg hash — simpler: | ||
| // register the manager first, then the registry with the manager address.) |
There was a problem hiding this comment.
Do I understand correctly that we think there's a simpler way to do this, but we are choosing to not do it? Why?
| // --------------------------------------------------------------------------- | ||
| // Auth-flow guard: the registry's manager-only function must reject calls | ||
| // that come from somewhere other than the manager contract. | ||
| // --------------------------------------------------------------------------- |
There was a problem hiding this comment.
Seems like a misplaced comment (it is not relevant to the approved_proposal_cannot_be_replayed test below)
…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(®istry, &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>
Drop the registry CLI, registry contracts, and contract test fixtures — those now live in stellar-registry/cli and stellar-registry/contracts. Apply the scaffold-cli-focused changes from the prepared split: - Workspace: keep crates/* and the scaffold-test fixture contracts only; drop the stellar-registry workspace dep. - READMEs, CONTRIBUTING.md, crate Cargo metadata: point at stellar-scaffold/cli. - CLAUDE.md: rewrite for this repo's scope. - init.rs FRONTEND_TEMPLATE: stellar-scaffold/ui (was the old theahaco repo). - justfile: trim to scaffold/reporter/fixture recipes; drop registry/contracts. - CI: - tests.yml scoped to stellar-scaffold-cli + stellar-scaffold-reporter. - cd-scaffold-cli.yml owner gate -> stellar-scaffold. - website.yml + website-release.yml: website/ renamed to docs/site/. - drop the registry-only workflows. - Restore .config/nextest.toml; add per-repo dependabot.yml. - Rename website/ -> docs/site/. - Drop the orphaned check-shell.sh. The Scaffold/Registry-area issue labeling, the open registry-area PRs (#510, #518), and the registry-area releases (registry-v*, stellar-registry-cli-v*) are handled out-of-band after this lands.
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](https://github.com/Consulting-Manao/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](https://stellar.expert/explorer/testnet/tx/359f0e46c15934de630c32e7bf6a405c852fccce3103178a86142802bfb577f9) · registry `CB6MC7MA…WYVE` · manager `CCQ7TC7F…WMNSB` Original PR (left open for context): stellar-scaffold/cli#518 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com>
Summary
Adds
contracts/registry-tansu-manager/, a Soroban contract that gates manager-authed operations on the on-chain registry behind approved Tansu DAO proposals. Single entry point:trigger(proposal_id). Single transaction, end-to-end:Key properties:
tansu,project_key,registry) is set immutably in__constructor. Wrong-project callers can't piggyback —triggerreads the proposal under our configuredproject_keyand pre-authorizes only that one outcome.Tansu::register(…, maintainers=[manager])orupdate_config). That way the manager is the direct caller ofTansu.execute, so Tansu's internalmaintainer.require_auth()is satisfied by Soroban's contract-implicit auth — no auth entry needed and noenable_non_root_authorizationsimulator dance.manager.require_auth()is satisfied byenv.authorize_as_current_contract. The pre-auth is scoped to one specific(contract, fn, args)triple — nothing else gets authorized.if proposal.status != Active { panic }(rejected on replay with#402 ProposalActive) is the source of truth.Tansu's
Proposal/OutcomeContract/ProposalStatustypes are derived at compile time fromtansu-stub's wasm spec viastellar_registry::import_contract_client!(tansu_stub)—tansu-stubcarries the canonical contracttype layout, mirroring upstreamConsulting-Manao/tansu. We'd have liked tocontractimport!the live Tansu wasm directly, but its v2.0.2 spec emits both apub trait Contract(the interface) and a#[contracttype] pub struct Contract(used as a typed-Address wrapper inset_nqg_contractetc.) into the same generated module, which collides — tracked inConsulting-Manao/tansu#152. The stub also gains a Tansu-shapedexecute(maintainer, project_key, proposal_id, _, _) -> ProposalStatusthat auto-invokes the outcome the way real Tansu does, so harnesses that target the stub exercise the fulltriggerchain.Supporting pieces:
contracts/hello/— minimal hello-world contract used as the payload by all three e2e scripts.contracts/test/tansu-stub/— stub Tansu (now withexecute+ a#402 ProposalActiveerror to mirror Tansu's replay guard).Test plan
cargo test -p registry-tansu-manager— 1 constructor smoke test. The integration behavior is covered by the e2e scripts below.cargo test -p hello -p tansu-stub— passes.cargo clippy -p registry-tansu-manager -p tansu-stub --all-targets -- -D warnings— clean.cargo fmt --all -- --check— clean.just build— wasm builds forregistry,registry_tansu_manager,tansu_stub, andhello. Topo sort orderstansu-stubbefore the manager via[package.metadata.stellar] contract = true.contracts/registry-tansu-manager/e2e-testnet.sh— see below.contracts/registry-tansu-manager/e2e-fast-tansu-testnet.sh(~2.5min wall-clock vs ~48h on live Tansu) — see below.contracts/registry-tansu-manager/e2e-real-tansu-testnet.sh— phase 1 has run; phase 2 against live Tansu hasn't been re-verified post-pivot (24h voting + 24h execute timelock).Stub-based testnet verification (
e2e-testnet.sh)Ran
NETWORK=testnet ./contracts/registry-tansu-manager/e2e-testnet.shend-to-end. All steps green:hello.wasmand have admin-as-manager publish it on the author's behalf.tansu-stub.registry-tansu-managerpointing at stub + registry.Approveddeploy-proposal on the stub (outcome targetsregistry.deploy(...)).manager.trigger(proposal_id)— pre-authorizes the outcome viaauthorize_as_current_contract, callsstub.execute(self, project_key, proposal_id, _, _); the stub auto-invokes the outcome;registry.deployruns under the pre-auth; hello deploys.hello(world) → "world"on the freshly deployed contract.trigger→#402 ProposalActivefrom the stub (mirroring Tansu's status guard).Contracts deployed during the verification run:
CA72QP7N6RTTMRYLMT7PUSYPWRBJGWMR5YOFDOSNTUOTX63SYNKU4445CAG7UKI6FCZB6464GUDE2FRVEOZR5QRIZACNVNSQSA7UXTFMN3ZQMKRICCBXY322ZYY4RLS3WEBD7BZPTMR4WEDGNTIS3OXTWVVJ2M4IG4OTRW2UCATBV7SZS6Q57SKHKQWAGT3D2CBQQ522NUTZFKJPGTZJUSPYNKEBIIF4(registered ashello-1780000169)manager.triggertx (single-tx flow including stub.execute → registry.deploy):4c0bff33…ca87d.Fast Tansu-fork testnet verification (
e2e-fast-tansu-testnet.sh)A custom build of Tansu (same
Proposal/OutcomeContractwire shape; addsmin_voting_period: Option<u64>andexecute_delay: Option<u64>toregister) is deployed atCDK7JBIIP6E75HOYLGRGWAHQLT6JUNUXQ7GNOYS3NAP26GISUXJ26UONon testnet. This collapses the ~48h voting+timelock window into the script-configurableMIN_VOTING_PERIOD+EXECUTE_DELAY(defaults: 60s + 60s).End-to-end against that Tansu:
1–4. Register fresh project (
min_voting_period=60,execute_delay=60), add members, upload hello.wasm, deploy registry, deploy manager.5.
set_manager(manager)on the registry,update_config(maintainers=[manager])on Tansu so the manager is the sole project maintainer.6. Create proposal whose outcome is
registry.publish_hash(hello, …).7. Vote Approve, wait through voting + execute_delay (~2 min).
8.
manager.trigger(proposal_id)— one tx flips the proposal Approved, runs the outcome, registry publishes. Verify withregistry.fetch_hash.9. Replay →
#402 ProposalActive.manager.triggertx end-to-end (Tansu.execute + Proposal collateral refund + outcome auto-invocation + registry.publish_hash, all in one):df2c8bab…ea360.Live-Tansu testnet verification (
e2e-real-tansu-testnet.sh)Two-phase script that exercises the same flow against the live Tansu DAO on testnet (
CBXKUSLQPVF35FYURR5C42BPYA5UOVDXX2ELKIM2CAJMCI6HXG2BHGZA) — realregister/add_member/create_proposal/vote, full 24h voting + 24h timelock window:./e2e-real-tansu-testnet.sh setup— registers project, auto-registers.xlmdomain on SorobanDomain, deploys registry + manager,update_configto hand maintainership to the manager,create_proposalwithregistry.publish_hash(...)outcome, vote Approve, save state../e2e-real-tansu-testnet.sh finalize <state-file>— after voting + timelock ends, callsmanager.trigger(proposal_id)and assertsregistry.fetch_hash.Phase 1 was verified on 2026-05-28 (see earlier commit messages). The trigger pivot landed today; phase 2 against live Tansu hasn't been re-run end-to-end yet (requires a fresh setup + 48h wait).
Out of scope (deliberate)
MultipleOutcomesfor v1.try_invoke_contracton the registry call from inside the manager. Withauthorize_as_current_contract, the registry's call is invoked by Tansu (not the manager), and a failure there propagates through Tansu's owntry_invoke_contractas#403 OutcomeError. Acceptable for v1.__check_authdesign. Equivalent security model but blocked by stellar-cli 26.0.0 not exposingenable_non_root_authorization(recording-mode simulation can't synthesize a non-root auth entry for a custom account whose__check_authwould satisfy it). Revisit when CLI support lands; thetriggerdesign works with current tooling and standard frontend SDKs.