feat: tansu-DAO-gated registry manager contract#5
Open
willemneal wants to merge 16 commits into
Open
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>
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(®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>
…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>
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.
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 viaenv.authorize_as_current_contract, then callsTansu.execute— so the outcome (e.g.registry.publish_hash/registry.deploy) lands in one transaction, satisfying the registry'smanager.require_auth()without any non-root auth recording.contracts/test/tansu-stub/— a Tansu stand-in that matches Tansu's wire format (get_proposal+ aexecutethat auto-invokes the outcome and enforces theProposalActivereplay guard), letting the flow be exercised without live Tansu.Follow-up wiring (one commit on top of the moved history)
contracts/registry-tansu-managerto the workspace members; the stub is already covered bycontracts/test/*.just buildnow usesstellar scaffold buildso wasm is staged totarget/stellar/local/, which both the registry tests'contractimport!and the manager'simport_contract_client!(tansu_stub)resolve against.setupbinstallsstellar-scaffold-cli.helloexample payload (not moved). The e2e scripts now publish/deploy the registry contract's own wasm:e2e-testnet.shdeploys a subregistry through the proposal (exercising the registry's 3-arg__constructor) and verifies with a read-onlymanager()call;e2e-fast/e2e-realpublish the registry wasm under the nameregistry.Verification
just build— all 6 contract wasms build and stage totarget/stellar/local/.just test— 45 tests pass (incl. the manager'sconstructor_stores_values).cargo clippy --all(+--tests --all-features) clean under the repo's-Dclippy::pedantic -Dwarnings.cargo fmt --all -- --checkclean.rust+Tests). CI now also installsstellar-scaffold(needed byjust build) andnextestintests.yml.Live testnet e2e ✅
Ran
e2e-fast-tansu-testnet.shagainst testnet with the registry wasm as the swapped payload — full DAO-gated flow passed end-to-end against the custom testnet Tansu:Add registry@0.1.0), voted Approve, waited out the voting period + execute delay.manager.triggerdroveTansu.execute → registry.publish_hashin a single tx (confirmed byproposal_executed/publishevents;wasm_name: "registry").registry resolved registry@0.1.0 -> f8b77e06…baa7050, and the replay guard rejected the second trigger withProposalActive.CB6MC7MA…WYVE· managerCCQ7TC7F…WMNSBOriginal PR (left open for context): stellar-scaffold/cli#518