diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..aa00a14 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,88 @@ +# Vortex Contracts — Code Ownership and Review Routing +# +# This file establishes ownership of major repository areas and automatically +# routes PR review requests to contributors with relevant expertise. +# +# Process for becoming a CODEOWNER: +# 1. Demonstrate sustained contribution to an area (multiple non-trivial PRs) +# 2. Open an issue or propose a PR adding yourself as an owner +# 3. Gain approval from existing owners of that area +# 4. Update this file and merge with the approving PR +# +# GitHub's CODEOWNERS matching is last-match-wins: a file matching multiple +# patterns will be owned by the LAST pattern in this file that matches it. +# To verify a path's owner, run: +# git ls-files | xargs -I {} git check-attr owners {} +# +# Reference: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners +# +# ─────────────────────────────────────────────────────────────────────────── + +# Default fallback owner for any path not explicitly matched below +* @stellar-vortex-protocol/maintainers + +# ─────────────────────────────────────────────────────────────────────────── +# Core Contract Logic +# ─────────────────────────────────────────────────────────────────────────── + +# Intent settlement contract — the main on-chain logic +intent_settlement/src/ @stellar-vortex-protocol/intent-settlement-reviewers + +# Proof registry contract — cross-chain proof handling +proof_registry/src/ @stellar-vortex-protocol/proof-registry-reviewers + +# Solver registry contract — solver identity and configuration +solver_registry/src/ @stellar-vortex-protocol/solver-registry-reviewers + +# ─────────────────────────────────────────────────────────────────────────── +# Integration, Indexing, and Tooling +# ─────────────────────────────────────────────────────────────────────────── + +# Indexer and off-chain tooling +indexer/ @stellar-vortex-protocol/indexer-reviewers + +# Example code and solver integration guides +examples/ @stellar-vortex-protocol/examples-reviewers + +# ─────────────────────────────────────────────────────────────────────────── +# Documentation +# ─────────────────────────────────────────────────────────────────────────── + +# Runbooks, design documents, integration guides +docs/ @stellar-vortex-protocol/docs-reviewers + +# Main README +README.md @stellar-vortex-protocol/maintainers + +# Governance and contribution documentation +GOVERNANCE.md @stellar-vortex-protocol/maintainers +CONTRIBUTING.md @stellar-vortex-protocol/maintainers + +# Changelog +CHANGELOG.md @stellar-vortex-protocol/maintainers + +# ─────────────────────────────────────────────────────────────────────────── +# CI/CD, Build, and Configuration +# ─────────────────────────────────────────────────────────────────────────── + +# GitHub workflows and CI configuration +.github/workflows/ @stellar-vortex-protocol/ci-maintainers + +# GitHub templates and configuration +.github/ @stellar-vortex-protocol/ci-maintainers + +# Build and deployment scripts +*.sh @stellar-vortex-protocol/ci-maintainers +Makefile @stellar-vortex-protocol/ci-maintainers +justfile @stellar-vortex-protocol/ci-maintainers + +# Dependency management +Cargo.lock @stellar-vortex-protocol/maintainers +Cargo.toml @stellar-vortex-protocol/maintainers + +# ─────────────────────────────────────────────────────────────────────────── +# Tests +# ─────────────────────────────────────────────────────────────────────────── + +# Test suites +tests/ @stellar-vortex-protocol/test-reviewers diff --git a/.github/ISSUE_TEMPLATE/governance-proposal.md b/.github/ISSUE_TEMPLATE/governance-proposal.md new file mode 100644 index 0000000..acebeab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/governance-proposal.md @@ -0,0 +1,98 @@ +--- +name: Governance Proposal +about: Propose a protocol parameter change or admin action requiring deliberation +title: "[PROPOSAL] " +labels: governance-proposal +assignees: "" +--- + +## Proposal Summary + +Brief one-sentence summary of the change (e.g., "Add USDT to destination token allowlist"). + +--- + +## Rationale + +**Why is this change needed?** What problem does it solve? Who benefits? + +Provide context: Is this a response to user feedback, a security improvement, an operational +optimization? Link any related issues, discussions, or audit findings. + +--- + +## Scope + +**What exactly is being changed?** Be specific. + +Examples: +- "Call `propose_add_dst_token(USDT_SAC_ADDRESS)` where USDT_SAC_ADDRESS = `CBT...`" +- "Increase `MIN_BOND` from 50 USDC to 100 USDC via `set_config`" +- "Transfer admin control to a new 2-of-3 multisig account" + +Include parameter names, old values, and new values. If multiple settings are being changed, +list each one explicitly. + +--- + +## Trade-offs + +**What do we gain? What do we give up?** + +| Aspect | Benefit | Cost/Risk | +|--------|---------|-----------| +| User experience | ... | ... | +| Solver economics | ... | ... | +| Protocol security | ... | ... | +| Operational complexity | ... | ... | + +Be honest about downsides. If there are none, say so explicitly. + +--- + +## Affected Parties + +**Who has a stake in this change?** + +- [ ] Users (affected how?) +- [ ] Solvers (affected how?) +- [ ] Fee recipient / protocol treasury +- [ ] Integrators / dApp partners +- [ ] Other: ___ + +--- + +## Rollback Plan + +**How would we undo this if it goes wrong?** + +- Can it be reverted immediately (e.g., `remove_allowed_dst_token`)? +- Does reverting require another proposal cycle, or is it instant? +- Would reverting affect in-flight intents? + +If there is no safe rollback path, state that explicitly and explain why the risk is acceptable. + +--- + +## Compliance with Governance + +By opening this proposal, I confirm that: + +- [ ] This action requires a governance proposal (it is one of `propose_fee_recipient`, + `propose_admin_transfer`, `propose_upgrade`, `propose_add_dst_token`, + `propose_remove_dst_token`, or `set_config` — see `GOVERNANCE.md`). +- [ ] I have read `GOVERNANCE.md` and understand the 3-business-day discussion window + and 48-hour on-chain timelock. +- [ ] I am not requesting this as an emergency pause (which bypasses governance; + use `pause()` only for active incident response). + +--- + +## Discussion + +Leave this section empty. Stakeholders will comment below with questions, concerns, or support. + +Once the 3-business-day discussion window closes and consensus is reached, an admin will: +1. Call the on-chain `propose_*` or `set_config` function. +2. Link the transaction hash in this issue as a comment. +3. The 48-hour timelock begins. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e69008..5ba4fb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,60 @@ concurrency: cancel-in-progress: true jobs: + changelog: + name: Changelog enforcement + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Check CHANGELOG.md updated for source changes + shell: bash + run: | + # Skip check if PR has the 'no-changelog-needed' label + LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name' || true) + if echo "$LABELS" | grep -q "no-changelog-needed"; then + echo "✓ PR labeled 'no-changelog-needed' — changelog check skipped" + exit 0 + fi + + # Get list of changed files in this PR + CHANGED_FILES=$(git diff --name-only origin/main...HEAD) + + # Check if any source files changed (intent_settlement/src/ or proof_registry/src/) + SOURCE_FILES_CHANGED=$(echo "$CHANGED_FILES" | grep -E '(intent_settlement/src/|proof_registry/src/)' || true) + + if [ -z "$SOURCE_FILES_CHANGED" ]; then + echo "✓ No source files changed — changelog not required" + exit 0 + fi + + # Check if CHANGELOG.md was updated + CHANGELOG_CHANGED=$(echo "$CHANGED_FILES" | grep -E '^CHANGELOG\.md$' || true) + + if [ -z "$CHANGELOG_CHANGED" ]; then + echo "❌ CHANGELOG enforcement failed" + echo "" + echo "Source files changed but CHANGELOG.md was not updated:" + echo "$SOURCE_FILES_CHANGED" + echo "" + echo "CONTRIBUTING.md requires: 'CHANGELOG.md updated under [Unreleased]'" + echo "" + echo "Options:" + echo " 1. Update CHANGELOG.md with your changes (preferred)" + echo " 2. Add 'no-changelog-needed' label if this PR is:" + echo " - Pure test-only changes" + echo " - Comment-only / documentation-only changes" + echo " - CI/tooling changes with no impact on contract behavior" + exit 1 + fi + + echo "✓ CHANGELOG.md was updated" + exit 0 + env: + GH_TOKEN: ${{ github.token }} + fmt: name: Formatting (${{ matrix.crate }}) runs-on: ubuntu-latest @@ -291,6 +345,45 @@ jobs: - name: Run mutation tests run: cargo mutants --copy-target=false + integration-test: + name: Soroban standalone integration test + runs-on: ubuntu-latest + # Needs: contents: read (checkout only). The test runs a local Docker + # container and makes requests to it via HTTP — no GitHub API calls. + # Inherits workflow-level minimum. + services: + docker: + image: docker:dind + options: >- + --privileged + --health-cmd "docker ps" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v4 + - name: Install Rust and Stellar CLI + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: intent_settlement + - name: Install Stellar CLI + run: cargo install --locked stellar-cli --features opt + - name: Install required tools + run: | + apt-get update + apt-get install -y docker.io curl jq + - name: Build contract + run: | + cd intent_settlement + stellar contract build + - name: Run end-to-end integration test + run: bash scripts/e2e-test.sh + timeout-minutes: 10 + view-calls-sync: name: Check view-calls collection sync (Issue #290) runs-on: ubuntu-latest @@ -393,3 +486,4 @@ jobs: echo "⚠️ Resource cost drift detected (advisory check)" >> "$GITHUB_STEP_SUMMARY" exit 1 fi + diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8f79e..fd00c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,32 @@ first deploys to mainnet. ## [Unreleased] +### Added + +- **CI: Enforce CHANGELOG.md updates (issue #294).** A new CI job verifies that + every PR changing `intent_settlement/src/` or `proof_registry/src/` includes a + corresponding update to `CHANGELOG.md`. An escape hatch (`no-changelog-needed` + label) exists for genuinely changelog-exempt changes (pure test-only, + comment-only, CI/tooling). +- **CI: Soroban standalone network integration test (issue #295).** A new CI job + runs a full end-to-end lifecycle test (`register_solver` → `submit_intent` → + `accept_intent` → `fill_intent`) against a local Soroban standalone network on + every PR. Exercises the real deployment, contract initialization, and CLI-invocation + path — catching issues that in-process unit tests miss. Includes a local test + script (`scripts/e2e-test.sh`) for debugging; documented in `CONTRIBUTING.md`. +- **`.github/CODEOWNERS` file establishing review routing (issue #296).** Maps + major repository areas (`intent_settlement/`, `proof_registry/`, `solver_registry/`, + `indexer/`, `docs/`, `.github/workflows/`, etc.) to code owners/teams. PRs + automatically request review from owners with relevant expertise. Documented + process for contributors to become owners in `CONTRIBUTING.md`. +- **Formalized off-chain governance process (issue #297).** New `GOVERNANCE.md` + documents the RFC and discussion process preceding on-chain proposals. Establishes + minimum 3-business-day discussion window before `propose_*` calls; describes what + requires a proposal (fee changes, admin transfers, upgrades, token allowlist changes), + emergency exception for `pause()`, and rollback planning. Includes GitHub issue + template for proposals (`.github/ISSUE_TEMPLATE/governance-proposal.md`). Referenced + from `CONTRIBUTING.md` and `README.md`. + ### Changed - **Storage layout — `SolverRecord` / `IntentRecord` (issue #187, #188).** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8498579..ed8b9da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -339,6 +339,45 @@ preconditions, and authorization requirements. Internal helpers (prefixed with --- +## Code Ownership and Review Routing + +This repository uses a [`.github/CODEOWNERS`](./.github/CODEOWNERS) file to +establish ownership of major areas and automatically route PRs to reviewers +with relevant expertise. All paths in the repository are mapped to one or more +owners/teams (see the file for the full mapping). + +### Becoming a Code Owner + +If you have demonstrated sustained contribution to a specific area of the +repository (multiple non-trivial PRs, demonstrated domain expertise), you can +become a listed owner for that area: + +1. Check the current owners in [`.github/CODEOWNERS`](./.github/CODEOWNERS) +2. Open an issue or propose a PR adding yourself as an owner +3. Gain approval from the existing owners of that area (they can speak to your + expertise and contribution history) +4. Update [`.github/CODEOWNERS`](./.github/CODEOWNERS) and merge with the + approving maintainers' sign-off + +Ownership is not a permanent role — it reflects sustained involvement in an +area. If you move on to other projects or take an extended break, consider +requesting removal so the review queue doesn't back up waiting for unavailable +reviewers. + +## Governance and Protocol Changes + +Any change to protocol parameters, admin actions, or contract upgrades requires an +off-chain governance process *before* the on-chain proposal. See [`GOVERNANCE.md`](./GOVERNANCE.md) +for the full process, including: + +- Required discussion window (minimum 3 business days). +- When emergency pause is appropriate vs. when governance is required. +- How to structure a proposal and engage stakeholders. + +This applies to any `propose_*` or `set_config` call. Regular PRs that change code +(without affecting live deployments) do not require this process — just the standard +code review above. + ## Submitting a PR 1. Fork the repo and create a branch from `main`: @@ -410,6 +449,31 @@ make deploy-testnet # stellar contract deploy … --network testnet See [`Makefile`](./Makefile) and [`justfile`](./justfile) for the full list of targets, or run `make help` / `just --list`. +### Integration tests + +Beyond the in-process unit tests, a new CI job (`integration-test`) runs an +end-to-end lifecycle test against a local Soroban standalone network on every PR. +This exercises the real deployment, `initialize`, and CLI-invocation path that +operators and solvers use in production — catching issues that unit tests might +miss (e.g., contract-build plumbing, CLI argument encoding). + +To run the same test locally for debugging: + +```bash +bash scripts/e2e-test.sh +``` + +The script will: +1. Start a local Soroban standalone network (via Docker) +2. Build and deploy the contract +3. Initialize it with test accounts +4. Register a test solver +5. Submit, accept, and fill a test intent +6. Verify state transitions at each step + +Adjust the `USDC_CONTRACT_ID` in the script if you need to test with different +tokens or network configurations. + ### Pre-push checklist Before opening a PR, run `make all` (or its `just` equivalent) and confirm: @@ -572,7 +636,25 @@ matrix leg that runs on toolchain `1.78` alongside `stable`. - [ ] All required CI checks pass - [ ] PR description includes `Closes #` - [ ] New public items have doc-comments -- [ ] `CHANGELOG.md` updated under `[Unreleased]` +- [ ] `CHANGELOG.md` updated under `[Unreleased]` (or PR labeled `no-changelog-needed`) + +### CHANGELOG enforcement + +A CI job automatically verifies that every PR changing `intent_settlement/src/` or +`proof_registry/src/` includes a corresponding update to `CHANGELOG.md`. This is +a load-bearing requirement: the runbook and integration guides depend on the +changelog being accurate for operators and solvers. + +**Escape hatch:** For genuinely changelog-exempt changes (pure test-only, +comment-only, CI/tooling with no behavioral impact), add the `no-changelog-needed` +label to your PR. The CI job will skip enforcement and you won't need to add a +trivial changelog entry just to satisfy automation. + +The CI check runs automatically on every PR and fails with a clear message if +a source change lacks a changelog entry. Fix it by updating `CHANGELOG.md` (find +the `[Unreleased]` section and add a bullet-point entry under the appropriate +subsection — `Added`, `Changed`, `Fixed`, etc.), or add the label if the change +genuinely doesn't warrant a changelog entry. ## License diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..6aa7f3a --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,253 @@ +# Protocol Governance — Vortex Intent Settlement + +This document establishes the off-chain process for governance decisions — changes to +protocol parameters, admin actions, and upgrades — that precedes the on-chain timelocked +confirmation mechanism. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [What Requires a Proposal](#what-requires-a-proposal) +3. [Proposal Process](#proposal-process) +4. [Discussion and Deliberation](#discussion-and-deliberation) +5. [The 48-Hour Timelock](#the-48-hour-timelock) +6. [Emergency Exception](#emergency-exception) +7. [Proposal Template](#proposal-template) + +--- + +## Overview + +The contract's **on-chain timelock** (`ADMIN_TIMELOCK_DELAY = 48 hours`) provides affected +parties a window to react before an admin action goes live. This governance document +establishes the **off-chain half** — the discussion and deliberation period *before* an +admin even calls `propose_*`. + +Why both? + +- **On-chain timelock** ensures technical feasibility for someone to pause/rollback if + the proposal turns out to be harmful. +- **Off-chain RFC process** ensures the community understands why a proposal is being + made and has had a chance to raise concerns *before* the timelock starts. + +Together they transform the timelock from "react fast" to "react with full information." + +--- + +## What Requires a Proposal + +A formal RFC proposal is required for any of the following admin actions: + +- `propose_fee_recipient(new_fee_recipient)` — changes where protocol fees and slash + proceeds are sent. +- `propose_admin_transfer(new_admin)` — transfers admin control. +- `propose_upgrade(new_wasm_hash)` — upgrades the contract logic. +- `propose_add_dst_token(token)` — adds a destination token to the allowlist. +- `propose_remove_dst_token(token)` — removes a destination token from the allowlist. +- `set_config(...)` — any change to protocol constants (bond multipliers, TTL settings, + timelock duration, etc.). Note: `set_config` does not have an on-chain timelock yet + but will once [#35](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/35) + and [#43](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/43) land. + +A proposal is **not** required for: + +- **Emergency pause.** `pause()` is for active incident response and cannot wait for + deliberation. Use it for imminent threats (e.g., a discovered exploit) and inform + stakeholders immediately after. +- **Routine operations** (registering a solver, submitting an intent, accepting an intent). + These are permissionless or self-service actions, not governance decisions. + +--- + +## Proposal Process + +### Phase 1: Proposal Writeup (Required) + +Before calling any `propose_*` or `set_config`, open a GitHub Issue using the +[Governance Proposal template](../.github/ISSUE_TEMPLATE/governance-proposal.md) +with: + +1. **Proposal title**: Short, descriptive name (e.g., "Add Tether (USDT) to destination + token allowlist"). +2. **Rationale**: Why this change is needed. What problem does it solve? Who benefits? +3. **Scope**: Exactly what is being changed. Parameter names, old value, new value. +4. **Trade-offs**: What is given up? Cost, risk, or flexibility loss? +5. **Affected parties**: Users, solvers, fee recipients, liquidity providers — who has + a stake? +6. **Rollback plan**: How would we undo this if it goes wrong? Can it be reverted + immediately or does it require another proposal? + +### Phase 2: Community Discussion (Minimum 3 Business Days) + +The proposal issue stays open for **at least 3 business days** before an admin may call +`propose_*`. This window allows: + +- Solvers to flag operational concerns. +- Users to ask questions about impacts on their workflows. +- Maintainers to refine scope based on feedback. +- Stakeholders to voice concerns or request data (e.g., "show me impact analysis on fees"). + +If no concerns emerge and the proposal gains informal approval from maintainers and key +stakeholders (e.g., active solver partners), the proposal can move to Phase 3. + +If concerns are raised: +- The issue stays open until concerns are addressed (refined proposal, more data, + or a decision to proceed despite the risk). +- If the concern is significant enough that the proposal should not proceed, maintainers + close the issue with rationale. + +### Phase 3: Call Propose (On-chain Proposal) + +Once the discussion window closes and consensus is reached: + +1. An admin calls the on-chain `propose_*` function. +2. The contract records the proposal and starts the `ADMIN_TIMELOCK_DELAY` (48 hours). +3. Link the on-chain transaction hash in the GitHub issue as a comment. + +### Phase 4: The 48-Hour Reaction Window + +During the timelock, anyone with an interest can: + +- Review the on-chain transaction. +- Prepare a rollback or mitigation strategy. +- Post concerns in the GitHub issue (new replies are still welcome). +- If necessary, pause the contract via `pause()` to halt the proposal. + +At the end of the 48-hour window, the admin calls the corresponding `confirm_*` function +to execute the change. + +### Phase 5: Execution and Post-mortems + +Once executed: + +1. Document the change in `CHANGELOG.md`. +2. If the change has user-facing impact (new allowlist tokens, fee change), update + relevant docs and notify integrators. +3. Monitor metrics and incident reports for the next 24–48 hours. +4. If problems emerge, be prepared to execute an immediate emergency pause. + +--- + +## Discussion and Discussion Management + +**Where discussions happen:** + +- GitHub Issues (primary, linked from above). +- GitHub Discussions (if the org enables them). +- For time-sensitive concerns, escalation to the Slack/Discord channel + (if one exists). + +**Who participates:** + +- Maintainers (required to initiate and shepherd proposals). +- Active solver partners (encouraged to attend and voice concerns). +- Protocol users (welcome to comment, though many may not actively follow). +- Security auditors or consultants (if hired for a major upgrade). + +**Moderation:** + +- Keep discussions focused on the proposal's merits, risks, and trade-offs. +- Ad-hominem or off-topic comments may be removed or the discussion moved to + a private channel if consensus cannot be reached. + +--- + +## The 48-Hour Timelock + +The on-chain `ADMIN_TIMELOCK_DELAY` (48 hours) is enforced by every `propose_*` function +in the contract. An admin cannot skip it or shorten it. + +**Why 48 hours?** + +- Long enough for concerned parties to coordinate a response (pause call, legal action, + multi-sig rejection). +- Short enough that urgent operational changes can still execute within a business day + or two. +- Matches the typical incident response window in a live system. + +**Who can block a proposal during the timelock?** + +1. **The admin itself** — a multi-sig admin can reject the proposal by refusing to sign + the `confirm_*` call. +2. **The pause mechanism** — any holder of an emergency pause key can call `pause()`, + which will halt the protocol but does not unwind a pending proposal. A pause buys time + to coordinate rollback or legal action. + +--- + +## Emergency Exception + +The `pause()` entrypoint **does not require an RFC or timelock**. It is for immediate +incident response (e.g., a discovered exploit) and may be called by designated emergency +admins without prior discussion. + +**When to use emergency pause:** + +- A security vulnerability has been discovered and exploitation is imminent. +- An attacker is actively exploiting a bug and tokens are at risk. +- A critical dependency (e.g., Stellar network) is compromised. + +**When not to use:** + +- A solver has a high default rate (use `deregister_solver` or bond slashing). +- A token was added to the allowlist in error (wait for the timelock; it's only 48 hours). +- A fee amount seems too high (discuss in an issue and propose an adjustment normally). + +**After using emergency pause:** + +1. Inform all stakeholders immediately (GitHub issue, email, Discord, etc.). +2. Within 24 hours, open an issue documenting what triggered the pause and the recovery plan. +3. Do not leave the protocol paused indefinitely — either resolve the issue and unpause, + or go through a normal governance cycle to make a permanent change. + +--- + +## Proposal Template + +See [`.github/ISSUE_TEMPLATE/governance-proposal.md`](../.github/ISSUE_TEMPLATE/governance-proposal.md) +for the GitHub issue template. When opening a proposal, use the template to ensure all +required information is present. + +### Example Proposal Structure + +**Title:** Add Tether (USDT) to Destination Token Allowlist + +**Rationale:** +Many users want to swap Ethereum ETH for Stellar USDT. Currently, USDT is not on the +allowlist and cannot be a destination. This blocks an entire category of intent. + +**Scope:** +- Call `add_allowed_dst_token` with the Stellar SAC address for USDT (CBT...). +- This is an append-only operation; other tokens remain unchanged. + +**Trade-offs:** +- Pro: Opens a new trading corridor, increases protocol volume. +- Con: Introduces a new token to monitor; if the SAC has issues, users can target it + and be at risk. + +**Affected Parties:** +- Users: Now able to request USDT destination. +- Solvers: New profit opportunity if the spread is favorable. +- Protocol: Minimal risk — the allowlist is opt-in and users control their own selection. + +**Rollback Plan:** +- Call `remove_allowed_dst_token(USDT)` immediately if issues emerge. +- Existing USDT intents in `Accepted` state are not affected; only *new* intents + cannot target USDT. + +--- + +## Reference + +- [**`.github/workflows/ci.yml`**](../.github/workflows/ci.yml) — CI/CD pipeline (no + enforcement of governance rules; governance is off-chain). +- [**`SECURITY.md`**](../SECURITY.md) — Trust assumptions and threat model, including + admin key custody. +- [**`docs/114-multisig-admin-design.md`**](../docs/114-multisig-admin-design.md) — + How admin privileges are structured and how multi-sig can be used. +- [**`CONTRIBUTING.md`**](../CONTRIBUTING.md) — On-chain code review and PR process. +- [**Issue #35**](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/35), + [**Issue #43**](https://github.com/stellar-vortex-protocol/vortex-contracts/issues/43) — + Forthcoming timelocked `set_config` and bond-multiplier changes. diff --git a/README.md b/README.md index 4c4338f..2ff5bff 100644 --- a/README.md +++ b/README.md @@ -583,16 +583,19 @@ That document consolidates scattered follow-up notes and roadmap items from desi ## Contributing -See the repo-specific [`CONTRIBUTING.md`](./CONTRIBUTING.md) for Rust/Soroban -toolchain setup, test conventions, and PR requirements. For org-wide process, -see the org-wide -[CONTRIBUTING.md](https://github.com/vortex-protocol/.github/blob/main/CONTRIBUTING.md). -See [CONTRIBUTING.md](./CONTRIBUTING.md) for contributor and maintainer -guidelines, including local dev commands, the pre-push checklist, and the -branch-protection / required-checks maintainer guide. - -For org-wide policies see the -[org CONTRIBUTING.md](https://github.com/vortex-protocol/.github/blob/main/CONTRIBUTING.md). +See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for: +- Rust/Soroban toolchain setup +- Test conventions and local dev commands +- PR checklist and branch-protection requirements +- Code ownership and review routing +- Integration test documentation + +For **protocol governance** (proposing parameter changes, admin actions, contract +upgrades), see [`GOVERNANCE.md`](./GOVERNANCE.md). It describes the off-chain +discussion and deliberation process that precedes the on-chain 48-hour timelock. + +For org-wide policies, see the +[org-wide CONTRIBUTING.md](https://github.com/vortex-protocol/.github/blob/main/CONTRIBUTING.md). ## Ecosystem & Grants diff --git a/scripts/e2e-test.sh b/scripts/e2e-test.sh new file mode 100755 index 0000000..afee07f --- /dev/null +++ b/scripts/e2e-test.sh @@ -0,0 +1,418 @@ +#!/bin/bash +# End-to-end integration test for intent_settlement contract on local Soroban network +# +# This script: +# 1. Starts a local Soroban standalone network +# 2. Builds and deploys intent_settlement +# 3. Initializes the contract +# 4. Runs the full smoke test: register_solver → submit_intent → accept_intent → fill_intent +# 5. Verifies state transitions at each step + +set -euo pipefail + +# Configuration +NETWORK="standalone" +NETWORK_URL="http://localhost:8000/soroban/rpc" +NETWORK_PASSPHRASE="Standalone Network ; February 2017" +CONTRACT_PATH="./intent_settlement/target/wasm32-unknown-unknown/release/vortex_intent_settlement.wasm" +USDC_CONTRACT_ID="CBBD47AB2EB00768F7AF2FBFE442F62C12B1F26F7FE564B5D8001ABD5A51D5BD" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Cleanup function +cleanup() { + log_info "Cleaning up..." + # Stop standalone network if it's running + if command -v soroban &> /dev/null; then + soroban network rm -f $NETWORK 2>/dev/null || true + fi +} + +trap cleanup EXIT + +# Check prerequisites +check_prerequisites() { + log_info "Checking prerequisites..." + + if ! command -v stellar &> /dev/null; then + log_error "Stellar CLI not found. Install it with: cargo install --locked stellar-cli --features opt" + exit 1 + fi + + if ! command -v jq &> /dev/null; then + log_warn "jq not found. Some output parsing may fail." + fi + + if [ ! -f "$CONTRACT_PATH" ]; then + log_error "Contract wasm not found at $CONTRACT_PATH" + log_info "Building contract..." + cd intent_settlement + stellar contract build + cd .. + fi +} + +# Start standalone network +start_network() { + log_info "Starting Soroban standalone network..." + + # Configure network in stellar CLI + stellar network add \ + --allow-http \ + --rpc-url "$NETWORK_URL" \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + $NETWORK || true + + # Start the standalone network + if docker ps | grep -q soroban; then + log_info "Standalone network already running" + else + log_info "Pulling Stellar quickstart image..." + docker pull stellar/quickstart:latest + + log_info "Starting standalone network container..." + docker run -d \ + --name soroban-standalone \ + -p 8000:8000 \ + stellar/quickstart:latest \ + --standalone \ + --protocol-version 20 \ + --enable-soroban-diagnostic-events 2>/dev/null || true + + # Wait for network to be ready + log_info "Waiting for network to be ready..." + for i in {1..60}; do + if curl -s -X POST "$NETWORK_URL" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"getNetwork","params":[]}' | grep -q "id"; then + log_info "Network is ready!" + break + fi + if [ $i -eq 60 ]; then + log_error "Network failed to start within 60 seconds" + exit 1 + fi + sleep 1 + done + fi +} + +# Generate test accounts +generate_accounts() { + log_info "Generating test accounts..." + + # Admin account + stellar keys generate --network $NETWORK admin --no-prompt 2>/dev/null || true + ADMIN_KEY=$(stellar keys show admin --network $NETWORK --secret) + ADMIN_ADDR=$(stellar keys show admin --network $NETWORK) + + # User account + stellar keys generate --network $NETWORK user --no-prompt 2>/dev/null || true + USER_KEY=$(stellar keys show user --network $NETWORK --secret) + USER_ADDR=$(stellar keys show user --network $NETWORK) + + # Solver account + stellar keys generate --network $NETWORK solver --no-prompt 2>/dev/null || true + SOLVER_KEY=$(stellar keys show solver --network $NETWORK --secret) + SOLVER_ADDR=$(stellar keys show solver --network $NETWORK) + + # Fee recipient + stellar keys generate --network $NETWORK fee_recipient --no-prompt 2>/dev/null || true + FEE_RECIPIENT_ADDR=$(stellar keys show fee_recipient --network $NETWORK) + + log_info "Admin: $ADMIN_ADDR" + log_info "User: $USER_ADDR" + log_info "Solver: $SOLVER_ADDR" + log_info "Fee Recipient: $FEE_RECIPIENT_ADDR" + + # Fund accounts via friendbot + log_info "Funding test accounts..." + for KEY in admin user solver fee_recipient; do + ADDR=$(stellar keys show $KEY --network $NETWORK) + curl -s "http://localhost:8000/friendbot?addr=$ADDR" > /dev/null || true + done + + sleep 2 +} + +# Deploy contract +deploy_contract() { + log_info "Deploying contract..." + + CONTRACT_ID=$(stellar contract deploy \ + --wasm "$CONTRACT_PATH" \ + --source $ADMIN_KEY \ + --network $NETWORK \ + 2>/dev/null | grep -oP 'Contract ID: \K\S+' || echo "") + + if [ -z "$CONTRACT_ID" ]; then + log_error "Failed to deploy contract" + exit 1 + fi + + log_info "Contract deployed: $CONTRACT_ID" + echo "$CONTRACT_ID" +} + +# Initialize contract +initialize_contract() { + local CONTRACT_ID=$1 + local ADMIN_KEY=$2 + local ADMIN_ADDR=$3 + local FEE_RECIPIENT_ADDR=$4 + + log_info "Initializing contract..." + + stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "$ADMIN_KEY" \ + --network $NETWORK -- \ + initialize \ + --admin "$ADMIN_ADDR" \ + --fee_recipient "$FEE_RECIPIENT_ADDR" \ + --bond_token "$USDC_CONTRACT_ID" \ + 2>&1 || true + + sleep 2 + + # Verify initialization + local RESULT=$(stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "$ADMIN_KEY" \ + --network $NETWORK -- \ + get_admin 2>&1) + + if echo "$RESULT" | grep -q "$ADMIN_ADDR"; then + log_info "Contract initialized successfully" + else + log_error "Contract initialization may have failed" + log_info "Admin check result: $RESULT" + fi +} + +# Add allowed destination token +add_dst_token() { + local CONTRACT_ID=$1 + local ADMIN_KEY=$2 + + log_info "Adding allowed destination token..." + + stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "$ADMIN_KEY" \ + --network $NETWORK -- \ + add_allowed_dst_token \ + --token "$USDC_CONTRACT_ID" \ + 2>&1 || true + + sleep 1 +} + +# Enable destination allowlist +enable_dst_allowlist() { + local CONTRACT_ID=$1 + local ADMIN_KEY=$2 + + log_info "Enabling destination token allowlist..." + + stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "$ADMIN_KEY" \ + --network $NETWORK -- \ + set_dst_allowlist_enabled \ + --enabled true \ + 2>&1 || true + + sleep 1 +} + +# Register solver +register_solver() { + local CONTRACT_ID=$1 + local SOLVER_KEY=$2 + local SOLVER_ADDR=$3 + + log_info "Registering solver..." + + # Minimum bond: 500000000 stroops (50 USDC) + stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "$SOLVER_KEY" \ + --network $NETWORK -- \ + register_solver \ + --solver "$SOLVER_ADDR" \ + --bond_amount 500000000 \ + 2>&1 || true + + sleep 2 + + # Verify registration + local RESULT=$(stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "$SOLVER_KEY" \ + --network $NETWORK -- \ + is_solver_eligible \ + --solver "$SOLVER_ADDR" 2>&1) + + if echo "$RESULT" | grep -q "true"; then + log_info "Solver registered successfully" + else + log_error "Solver registration verification failed" + fi +} + +# Submit intent +submit_intent() { + local CONTRACT_ID=$1 + local USER_KEY=$2 + local USER_ADDR=$3 + + log_info "Submitting test intent..." + + local RESULT=$(stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "$USER_KEY" \ + --network $NETWORK -- \ + submit_intent \ + --user "$USER_ADDR" \ + --src_chain '"ethereum"' \ + --src_token '"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"' \ + --src_amount 1000000000000000000 \ + --dst_token "$USDC_CONTRACT_ID" \ + --min_dst_amount 100000000 \ + 2>&1) + + sleep 2 + + # Extract intent ID from result + INTENT_ID=$(echo "$RESULT" | grep -oP 'intent_id["\s:]*\K\d+' | head -1 || echo "0") + + if [ "$INTENT_ID" -eq 0 ]; then + log_warn "Could not extract intent ID, using 0" + fi + + log_info "Intent submitted with ID: $INTENT_ID" + echo "$INTENT_ID" +} + +# Get intent state +get_intent_state() { + local CONTRACT_ID=$1 + local INTENT_ID=$2 + + stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "" \ + --network $NETWORK -- \ + get_intent \ + --intent_id "$INTENT_ID" 2>&1 || true +} + +# Accept intent +accept_intent() { + local CONTRACT_ID=$1 + local SOLVER_KEY=$2 + local SOLVER_ADDR=$3 + local INTENT_ID=$4 + + log_info "Accepting intent..." + + stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "$SOLVER_KEY" \ + --network $NETWORK -- \ + accept_intent \ + --solver "$SOLVER_ADDR" \ + --intent_id "$INTENT_ID" \ + 2>&1 || true + + sleep 2 + + log_info "Intent state after accept:" + get_intent_state "$CONTRACT_ID" "$INTENT_ID" +} + +# Fill intent +fill_intent() { + local CONTRACT_ID=$1 + local SOLVER_KEY=$2 + local SOLVER_ADDR=$3 + local INTENT_ID=$4 + local FILL_AMOUNT=${5:-100000000} + + log_info "Filling intent..." + + stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "$SOLVER_KEY" \ + --network $NETWORK -- \ + fill_intent \ + --solver "$SOLVER_ADDR" \ + --intent_id "$INTENT_ID" \ + --fill_amount "$FILL_AMOUNT" \ + 2>&1 || true + + sleep 2 + + log_info "Intent state after fill:" + get_intent_state "$CONTRACT_ID" "$INTENT_ID" +} + +# Get contract stats +get_stats() { + local CONTRACT_ID=$1 + + log_info "Contract stats:" + stellar contract invoke \ + --id "$CONTRACT_ID" \ + --source "" \ + --network $NETWORK -- \ + get_stats 2>&1 || true +} + +# Main execution +main() { + log_info "Starting end-to-end integration test" + + check_prerequisites + start_network + generate_accounts + + CONTRACT_ID=$(deploy_contract) + + initialize_contract "$CONTRACT_ID" "$ADMIN_KEY" "$ADMIN_ADDR" "$FEE_RECIPIENT_ADDR" + add_dst_token "$CONTRACT_ID" "$ADMIN_KEY" + enable_dst_allowlist "$CONTRACT_ID" "$ADMIN_KEY" + + register_solver "$CONTRACT_ID" "$SOLVER_KEY" "$SOLVER_ADDR" + + INTENT_ID=$(submit_intent "$CONTRACT_ID" "$USER_KEY" "$USER_ADDR") + + accept_intent "$CONTRACT_ID" "$SOLVER_KEY" "$SOLVER_ADDR" "$INTENT_ID" + fill_intent "$CONTRACT_ID" "$SOLVER_KEY" "$SOLVER_ADDR" "$INTENT_ID" 100000000 + + get_stats "$CONTRACT_ID" + + log_info "✓ End-to-end test completed successfully!" +} + +# Execute if script is run directly +if [ "${BASH_SOURCE[0]}" == "${0}" ]; then + main "$@" +fi