Skip to content

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

Open
willemneal wants to merge 14 commits into
mainfrom
feat/registry-tansu-manager
Open

feat: tansu-DAO-gated registry manager contract#518
willemneal wants to merge 14 commits into
mainfrom
feat/registry-tansu-manager

Conversation

@willemneal

@willemneal willemneal commented May 19, 2026

Copy link
Copy Markdown
Contributor

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:

user.tx
  └── manager.trigger(proposal_id)
        ├── read proposal from Tansu under our project_key       ← gate
        ├── env.authorize_as_current_contract(outcome_call)      ← pre-auth
        └── invoke_contract(Tansu, "execute", [self, project_key, id, _, _])
              ├── auth_maintainers: manager.require_auth         ← implicit (direct caller)
              ├── tally votes → flip status → Approved
              └── try_invoke_contract(outcome)                   ← registry.publish_hash/deploy(args)
                    └── manager.require_auth                     ← matched by pre-auth
                          → outcome runs in the same tx

Key properties:

  • Config (tansu, project_key, registry) is set immutably in __constructor. Wrong-project callers can't piggyback — trigger reads the proposal under our configured project_key and pre-authorizes only that one outcome.
  • The manager must be the Tansu project's sole maintainer (set up at deploy time via Tansu::register(…, maintainers=[manager]) or update_config). That way the manager is the direct caller of Tansu.execute, so Tansu's internal maintainer.require_auth() is satisfied by Soroban's contract-implicit auth — no auth entry needed and no enable_non_root_authorization simulator dance.
  • The registry's manager.require_auth() is satisfied by env.authorize_as_current_contract. The pre-auth is scoped to one specific (contract, fn, args) triple — nothing else gets authorized.
  • No on-manager replay guard. Tansu's own if proposal.status != Active { panic } (rejected on replay with #402 ProposalActive) is the source of truth.

Tansu's Proposal / OutcomeContract / ProposalStatus types are derived at compile time from tansu-stub's wasm spec via stellar_registry::import_contract_client!(tansu_stub)tansu-stub carries the canonical contracttype layout, mirroring upstream Consulting-Manao/tansu. We'd have liked to contractimport! the live Tansu wasm directly, but its v2.0.2 spec emits both a pub trait Contract (the interface) and a #[contracttype] pub struct Contract (used as a typed-Address wrapper in set_nqg_contract etc.) into the same generated module, which collides — tracked in Consulting-Manao/tansu#152. The stub also gains a Tansu-shaped execute(maintainer, project_key, proposal_id, _, _) -> ProposalStatus that auto-invokes the outcome the way real Tansu does, so harnesses that target the stub exercise the full trigger chain.

Supporting pieces:

  • contracts/hello/ — minimal hello-world contract used as the payload by all three e2e scripts.
  • contracts/test/tansu-stub/ — stub Tansu (now with execute + a #402 ProposalActive error 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 for registry, registry_tansu_manager, tansu_stub, and hello. Topo sort orders tansu-stub before the manager via [package.metadata.stellar] contract = true.
  • Stub-based testnet via contracts/registry-tansu-manager/e2e-testnet.sh — see below.
  • Custom-Tansu testnet via contracts/registry-tansu-manager/e2e-fast-tansu-testnet.sh (~2.5min wall-clock vs ~48h on live Tansu) — see below.
  • Live-Tansu testnet via 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.sh end-to-end. All steps green:

  1. Deploy fresh registry (admin as initial manager so the publisher can self-publish).
  2. Upload hello.wasm and have admin-as-manager publish it on the author's behalf.
  3. Deploy tansu-stub.
  4. Deploy registry-tansu-manager pointing at stub + registry.
  5. Admin swaps the registry's manager to the manager contract.
  6. Plant an Approved deploy-proposal on the stub (outcome targets registry.deploy(...)).
  7. manager.trigger(proposal_id) — pre-authorizes the outcome via authorize_as_current_contract, calls stub.execute(self, project_key, proposal_id, _, _); the stub auto-invokes the outcome; registry.deploy runs under the pre-auth; hello deploys.
  8. hello(world) → "world" on the freshly deployed contract.
  9. Re-trigger#402 ProposalActive from the stub (mirroring Tansu's status guard).

Contracts deployed during the verification run:

Role Address
registry CA72QP7N6RTTMRYLMT7PUSYPWRBJGWMR5YOFDOSNTUOTX63SYNKU4445
registry-tansu-manager CAG7UKI6FCZB6464GUDE2FRVEOZR5QRIZACNVNSQSA7UXTFMN3ZQMKRI
tansu-stub CCBXY322ZYY4RLS3WEBD7BZPTMR4WEDGNTIS3OXTWVVJ2M4IG4OTRW2U
hello (deployed via proposal) CATBV7SZS6Q57SKHKQWAGT3D2CBQQ522NUTZFKJPGTZJUSPYNKEBIIF4 (registered as hello-1780000169)

manager.trigger tx (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/OutcomeContract wire shape; adds min_voting_period: Option<u64> and execute_delay: Option<u64> to register) is deployed at CDK7JBIIP6E75HOYLGRGWAHQLT6JUNUXQ7GNOYS3NAP26GISUXJ26UON on testnet. This collapses the ~48h voting+timelock window into the script-configurable MIN_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 with registry.fetch_hash.
9. Replay → #402 ProposalActive.

manager.trigger tx 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) — real register / add_member / create_proposal / vote, full 24h voting + 24h timelock window:

  • ./e2e-real-tansu-testnet.sh setup — registers project, auto-registers .xlm domain on SorobanDomain, deploys registry + manager, update_config to hand maintainership to the manager, create_proposal with registry.publish_hash(...) outcome, vote Approve, save state.
  • ./e2e-real-tansu-testnet.sh finalize <state-file> — after voting + timelock ends, calls manager.trigger(proposal_id) and asserts registry.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)

  • Multi-outcome proposals. Rejected with MultipleOutcomes for v1.
  • try_invoke_contract on the registry call from inside the manager. With authorize_as_current_contract, the registry's call is invoked by Tansu (not the manager), and a failure there propagates through Tansu's own try_invoke_contract as #403 OutcomeError. Acceptable for v1.
  • Pure custom-account __check_auth design. Equivalent security model but blocked by stellar-cli 26.0.0 not exposing enable_non_root_authorization (recording-mode simulation can't synthesize a non-root auth entry for a custom account whose __check_auth would satisfy it). Revisit when CLI support lands; the trigger design works with current tooling and standard frontend SDKs.

willemneal and others added 2 commits May 19, 2026 13:46
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>
@willemneal willemneal changed the title feat/registry tansu manager feat: tansu-DAO-gated registry manager contract May 19, 2026
willemneal and others added 3 commits May 19, 2026 14:14
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>
Comment thread contracts/registry-tansu-manager/src/lib.rs Outdated
@chadoh

chadoh commented May 26, 2026

Copy link
Copy Markdown
Contributor

Also lands two supporting pieces for the e2e test:

  • contracts/hello/ — minimal hello-world contract used as the published+deployed payload.

Seems like this should live in contracts/test or some other test-specific directory

@chadoh chadoh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread contracts/registry-tansu-manager/e2e-testnet.sh Outdated
Comment thread contracts/registry-tansu-manager/e2e-testnet.sh Outdated
Comment thread contracts/registry-tansu-manager/e2e-testnet.sh Outdated
Comment on lines +101 to +113
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should these be combined to use stellar registry publish instead? Or to at least call invoke -- publish directly, instead of invoke -- publish_hash?

Comment on lines +117 to +131
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +11 to +13
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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)."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

  1. Create a new project
  2. Add minimum number of voters
  3. 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread Cargo.toml
Comment on lines 6 to 10
"contracts/registry",
"contracts/registry-tansu-manager",
"contracts/hello",
"contracts/test/*",
"crates/stellar-scaffold-test/fixtures/contracts/*",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 ¯\_(ツ)_/¯

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
chadoh previously approved these changes May 27, 2026

@chadoh chadoh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A couple small questions/notes, but nothing blocking!

Comment thread contracts/registry-tansu-manager/src/lib.rs Outdated
Comment on lines +161 to +165
let proposal: Proposal = env.invoke_contract(
&tansu,
&Symbol::new(env, "get_proposal"),
vec![env, project_key.into_val(env), proposal_id.into_val(env)],
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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(&registry, &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.

Comment on lines +90 to +93
// 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.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do I understand correctly that we think there's a simpler way to do this, but we are choosing to not do it? Why?

Comment on lines +307 to +310
// ---------------------------------------------------------------------------
// Auth-flow guard: the registry's manager-only function must reject calls
// that come from somewhere other than the manager contract.
// ---------------------------------------------------------------------------

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like a misplaced comment (it is not relevant to the approved_proposal_cannot_be_replayed test below)

willemneal and others added 4 commits May 28, 2026 00:24
…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>
chadoh and others added 4 commits May 28, 2026 13:31
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>
willemneal added a commit that referenced this pull request Jun 3, 2026
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.
willemneal added a commit to stellar-registry/contracts that referenced this pull request Jun 8, 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](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>
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.

3 participants