Skip to content

feat: per-project governance overrides (min_voting_period, execute_delay)#155

Open
willemneal wants to merge 9 commits into
Consulting-Manao:mainfrom
willemneal:feat/project-min-voting-period
Open

feat: per-project governance overrides (min_voting_period, execute_delay)#155
willemneal wants to merge 9 commits into
Consulting-Manao:mainfrom
willemneal:feat/project-min-voting-period

Conversation

@willemneal

@willemneal willemneal commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds two optional per-project governance overrides at registration:

  • min_voting_period: Option<u64> — minimum seconds between proposal creation and voting_ends_at. Defaults to the global MIN_VOTING_PERIOD (24h).
  • execute_delay: Option<u64> — seconds between voting_ends_at and when execute() becomes callable. Defaults to the global TIMELOCK_DELAY (24h).

Together these let a project drive a proposal end-to-end in seconds instead of the ~25-hour mainnet floor. Use cases: testnet flows, internal-tool DAOs, fast-iteration governance experiments.

Stacked on #154 — diff shows the contract-rename commits from that PR too; once #154 merges, the diff here will narrow.

API changes

fn register(
    env, maintainer, name, maintainers, url, ipfs,
    min_voting_period: Option<u64>,  // NEW
    execute_delay: Option<u64>,      // NEW
) -> Bytes;

fn get_min_voting_period(env, project_key) -> u64;  // NEW
fn get_execute_delay(env, project_key) -> u64;      // NEW
  • Both overrides validate Some(v) in 1..=MAX_VOTING_PERIOD (30 days). Out-of-range panics with new ContractErrors::InvalidVotingPeriod = 212.
  • Absence = use global default (no migration needed for already-registered projects).

Storage

Two new persistent ProjectKey entries:

  • ProjectKey::MinVotingPeriod(Bytes) -> u64
  • ProjectKey::ExecuteDelay(Bytes) -> u64

Where the overrides take effect

  • create_proposal reads MinVotingPeriod, falling back to MIN_VOTING_PERIOD, when computing the lower bound for voting_ends_at.
  • execute reads ExecuteDelay, falling back to types::TIMELOCK_DELAY, when gating proposal execution.
  • Intentionally unchanged: the admin-upgrade timelock in propose_upgrade (contract_tansu.rs:199) still uses the hardcoded TIMELOCK_DELAY. Different threat model — admin compromise window — and not something a project's maintainers should be able to shorten.

Test plan

  • cargo test --manifest-path contracts/tansu/Cargo.toml --release — 63 pass (54 existing + 9 new)
    • min_voting_period_defaults_when_unset
    • min_voting_period_override_is_stored_and_read
    • min_voting_period_rejects_zero
    • min_voting_period_rejects_above_max
    • min_voting_period_override_applies_to_create_proposal (7-day project rejects 2-day proposal, accepts 8-day)
    • execute_delay_defaults_when_unset
    • execute_delay_override_is_stored_and_read
    • execute_delay_rejects_zero
    • execute_delay_override_applies_to_execute (60-second override: execute() panics at voting_ends_at + 59s, succeeds at voting_ends_at + 60s — proving the 24-hour TIMELOCK_DELAY no longer gates the path)
  • cargo clippy --all-targets --all-features -- -Dwarnings
  • cargo fmt --check
  • cargo build --target wasm32v1-none --release
  • Regenerated and prettier-formatted TypeScript bindings (stellar contract bindings typescript).
  • Deployed to testnet: `CDK7JBIIP6E75HOYLGRGWAHQLT6JUNUXQ7GNOYS3NAP26GISUXJ26UON` — get_execute_delay returns 86400 on a fresh project (default ✓).

Out of scope

  • Updating either override on an existing project. The framing was "when creating a project"; a setter path is left for a follow-up if needed (e.g., adding both fields to update_config).
  • The admin-upgrade timelock stays hardcoded (see above).

🤖 Generated with Claude Code

@netlify

netlify Bot commented May 28, 2026

Copy link
Copy Markdown

👷 Deploy request for tansu pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 05e12bf

@netlify

netlify Bot commented May 28, 2026

Copy link
Copy Markdown

👷 Deploy request for staging-tansu pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 05e12bf

@willemneal willemneal force-pushed the feat/project-min-voting-period branch from 3d28f28 to 53aa944 Compare May 28, 2026 14:55
@willemneal willemneal changed the title feat: per-project minimum voting period override feat: per-project governance overrides (min_voting_period, execute_delay) May 28, 2026
Comment thread dapp/packages/tansu/dist/index.d.ts
Comment thread contracts/tansu/src/lib.rs Outdated
execute_delay: Option<u64>,
) -> Bytes;

fn get_min_voting_period(env: Env, project_key: Bytes) -> u64;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we really need these 2 in the public API? I don't have much simple accessor otherwise as you can see elsewhere. I tend to just inline storage calls.

VoterWeight = 209,
VoteLimitExceeded = 210,
VoterConflicted = 211,
InvalidVotingPeriod = 212,

@tupui tupui May 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There is a mapping file to update. I know that thing is not ideal. (The linter failure)

@tupui

tupui commented Jun 5, 2026

Copy link
Copy Markdown
Member

@willemneal Ping in case you missed my review. Otherwise LGTM

willemneal and others added 9 commits June 8, 2026 13:34
…ename

Regenerated via `stellar contract bindings typescript`. Function bodies
are unchanged at runtime — only the symbol name shifts from `Contract`
to `ContractRef` for the typed-address-plus-wasm-hash wrapper used by
`set_*_contract`. Formatting reflects current stellar-cli 26.0.0 output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run prettier --write on dapp/packages/tansu sources/dist generated by
stellar-cli's TS binding emitter, which doesn't match the project's
prettier config out of the box.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`register()` accepts an optional `min_voting_period: Option<u64>` (seconds).
When `Some(v)`, `v` must be in `1..=MAX_VOTING_PERIOD`; otherwise the global
24h default applies. The override is stored under a new
`ProjectKey::MinVotingPeriod(project_key)` storage entry so existing projects'
`Project` schema is unchanged.

`create_proposal` now reads the per-project override (falling back to the
default) when validating `voting_ends_at`. Added read accessor
`get_min_voting_period(project_key) -> u64` returning the effective value.

New `ContractErrors::InvalidVotingPeriod` (212) is raised at registration
for out-of-range overrides.

Includes:
- Unit tests for default lookup, override storage, zero / above-max rejection.
- End-to-end test: a 7-day project rejects a 2-day proposal and accepts an
  8-day one.
- Regenerated TypeScript bindings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-run prettier on dapp/packages/tansu after regenerating bindings
against the new wasm spec (now includes `min_voting_period` /
`get_min_voting_period`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a second `execute_delay: Option<u64>` parameter to `register`,
mirroring `min_voting_period`. When `Some(v)`, `v` must be in
`1..=MAX_VOTING_PERIOD` (reuses `InvalidVotingPeriod`); stored under
`ProjectKey::ExecuteDelay(project_key)`. `execute()` reads the
per-project value, falling back to `types::TIMELOCK_DELAY`. The
admin upgrade timelock in `propose_upgrade` is intentionally
unchanged — different threat model.

`get_execute_delay(project_key) -> u64` exposes the effective value.

Together with `min_voting_period`, this lets a fast-flow testnet
project drive a proposal end-to-end in <2 minutes instead of the
~25-hour mainnet floor.

New tests:
- `execute_delay_defaults_when_unset` / `_override_is_stored_and_read`
  / `_rejects_zero` in test_register.rs.
- `execute_delay_override_applies_to_execute` in test_dao.rs:
  a 60-second override lets `execute()` succeed at voting_ends_at + 60s
  and panic at voting_ends_at + 59s — proving the 24-hour TIMELOCK_DELAY
  no longer gates the path.

Regenerated TypeScript bindings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Accidentally included in the previous commit; this contract ID belongs
in my fork's local state, not in the upstream PR.

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

Rebasing onto upstream/main (which removed SorobanDomain via Consulting-Manao#157 and
replaced it with a collateral deposit) makes the previously generated
bindings stale. Regenerate them from the rebased contract:

  * drops the SorobanDomain client methods / error variants
  * keeps the collateral-based register() and the new governance params
  * adds InvalidVotingPeriod (212), now mapped in contractErrorMessages.ts

`register()` gained `min_voting_period` / `execute_delay`, so thread them
(optional) through `createProjectFlow` in FlowService.ts; otherwise the
dapp no longer type-checks against the regenerated bindings.

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

Per review on Consulting-Manao#155: the codebase inlines storage reads rather than exposing
simple accessors. The DAO already reads the per-project overrides directly from
storage (contract_dao.rs), and the dapp never queried these methods, so nothing
relied on them. Remove them from VersioningTrait; the affected tests now read
the stored value straight from storage via `as_contract`. Regenerate the
bindings to drop the two methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@willemneal willemneal force-pushed the feat/project-min-voting-period branch from 17a2b91 to 05e12bf Compare June 8, 2026 19:20
willemneal added a commit to willemneal/tansu that referenced this pull request Jun 8, 2026
…ay accessors

Per review on Consulting-Manao#155: the codebase inlines storage reads rather than exposing
simple accessors. The DAO already reads the per-project overrides directly from
storage (contract_dao.rs), and the dapp never queried these methods, so nothing
relied on them. Remove them from VersioningTrait; the affected tests now read
the stored value straight from storage via `as_contract`. Regenerate the
bindings to drop the two methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@willemneal willemneal force-pushed the feat/project-min-voting-period branch 2 times, most recently from fc1944d to 05e12bf Compare June 8, 2026 22:18
willemneal added a commit to willemneal/tansu that referenced this pull request Jun 10, 2026
…ay accessors

Per review on Consulting-Manao#155: the codebase inlines storage reads rather than exposing
simple accessors. The DAO already reads the per-project overrides directly from
storage (contract_dao.rs), and the dapp never queried these methods, so nothing
relied on them. Remove them from VersioningTrait; the affected tests now read
the stored value straight from storage via `as_contract`. Regenerate the
bindings to drop the two methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
willemneal added a commit to willemneal/tansu that referenced this pull request Jun 10, 2026
…ay accessors

Per review on Consulting-Manao#155: the codebase inlines storage reads rather than exposing
simple accessors. The DAO already reads the per-project overrides directly from
storage (contract_dao.rs), and the dapp never queried these methods, so nothing
relied on them. Remove them from VersioningTrait; the affected tests now read
the stored value straight from storage via `as_contract`. Regenerate the
bindings to drop the two methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
willemneal added a commit to willemneal/tansu that referenced this pull request Jun 10, 2026
…ay accessors

Per review on Consulting-Manao#155: the codebase inlines storage reads rather than exposing
simple accessors. The DAO already reads the per-project overrides directly from
storage (contract_dao.rs), and the dapp never queried these methods, so nothing
relied on them. Remove them from VersioningTrait; the affected tests now read
the stored value straight from storage via `as_contract`. Regenerate the
bindings to drop the two methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
willemneal added a commit to willemneal/tansu that referenced this pull request Jun 10, 2026
…ay accessors

Per review on Consulting-Manao#155: the codebase inlines storage reads rather than exposing
simple accessors. The DAO already reads the per-project overrides directly from
storage (contract_dao.rs), and the dapp never queried these methods, so nothing
relied on them. Remove them from VersioningTrait; the affected tests now read
the stored value straight from storage via `as_contract`. Regenerate the
bindings to drop the two methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
willemneal added a commit to willemneal/tansu that referenced this pull request Jun 10, 2026
…ay accessors

Per review on Consulting-Manao#155: the codebase inlines storage reads rather than exposing
simple accessors. The DAO already reads the per-project overrides directly from
storage (contract_dao.rs), and the dapp never queried these methods, so nothing
relied on them. Remove them from VersioningTrait; the affected tests now read
the stored value straight from storage via `as_contract`. Regenerate the
bindings to drop the two methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
willemneal added a commit to willemneal/tansu that referenced this pull request Jun 10, 2026
…ay accessors

Per review on Consulting-Manao#155: the codebase inlines storage reads rather than exposing
simple accessors. The DAO already reads the per-project overrides directly from
storage (contract_dao.rs), and the dapp never queried these methods, so nothing
relied on them. Remove them from VersioningTrait; the affected tests now read
the stored value straight from storage via `as_contract`. Regenerate the
bindings to drop the two methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
willemneal added a commit to willemneal/tansu that referenced this pull request Jun 10, 2026
…ay accessors

Per review on Consulting-Manao#155: the codebase inlines storage reads rather than exposing
simple accessors. The DAO already reads the per-project overrides directly from
storage (contract_dao.rs), and the dapp never queried these methods, so nothing
relied on them. Remove them from VersioningTrait; the affected tests now read
the stored value straight from storage via `as_contract`. Regenerate the
bindings to drop the two methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
willemneal added a commit to willemneal/tansu that referenced this pull request Jun 10, 2026
…ay accessors

Per review on Consulting-Manao#155: the codebase inlines storage reads rather than exposing
simple accessors. The DAO already reads the per-project overrides directly from
storage (contract_dao.rs), and the dapp never queried these methods, so nothing
relied on them. Remove them from VersioningTrait; the affected tests now read
the stored value straight from storage via `as_contract`. Regenerate the
bindings to drop the two methods.

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

@tupui tupui left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@willemneal what is the status here? Now it looks good to me and happy to get in. Just noting that the update config function does not let people adjust the timings. Which can be ok.

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