diff --git a/.env.deploy.example b/.env.deploy.example index 0e0eaaa..f3b5148 100644 --- a/.env.deploy.example +++ b/.env.deploy.example @@ -16,6 +16,6 @@ FEE_ACCOUNT= # Contract caps at 1000 (10%) per audit L4 (2026-06 Stellar-skill audit). FEE_BPS=250 -# Initial credit balance for newly-bootstrapped profiles (u32). -# Per boundless-credits-reputation-prd.md the default is 10. -BOOTSTRAP_CREDITS=10 +# Contract 1.0.0 also required BOOTSTRAP_CREDITS here. The 1.1.0 +# credit-removal upgrade (2026-06) moved credits to an off-chain ledger in +# boundless-nestjs; the profile constructor now takes only --admin. diff --git a/.github/workflows/contract-deploy.yml b/.github/workflows/contract-deploy.yml deleted file mode 100644 index 321aaa8..0000000 --- a/.github/workflows/contract-deploy.yml +++ /dev/null @@ -1,135 +0,0 @@ -name: Contract Deployment - -# DISABLED: this workflow targets the old single-contract model. -# Re-enable after the two-contract deploy script lands (events + profile with -# the cross-contract binding sequence per boundless-credits-reputation-prd.md -# Section 13.1). - -on: - workflow_dispatch: - inputs: - network: - description: 'Network to deploy to (testnet/mainnet)' - required: true - default: 'testnet' - type: choice - options: - - testnet - - mainnet - account: - description: 'Account to deploy from' - required: true - default: 'alice' - type: string - workflow_call: - secrets: - STELLAR_SECRET_KEY: - required: true - SOROBAN_SECRET_KEY: - required: true - CONTRACT_ID: - required: false - -permissions: - contents: read - actions: read - id-token: write - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Rust and Stellar - uses: ./.github/actions/setup-rust-stellar - - - name: Verify secrets - run: | - echo "Verifying secrets..." - if [ -n "${{ secrets.STELLAR_SECRET_KEY }}" ]; then - # Show first and last 4 characters of the secret key - SECRET_KEY="${{ secrets.STELLAR_SECRET_KEY }}" - FIRST_CHARS=${SECRET_KEY:0:4} - LAST_CHARS=${SECRET_KEY: -4} - echo "STELLAR_SECRET_KEY is set (length: ${#SECRET_KEY})" - echo "First 4 chars: $FIRST_CHARS" - echo "Last 4 chars: $LAST_CHARS" - else - echo "STELLAR_SECRET_KEY is not set!" - fi - - if [ -n "${{ secrets.SOROBAN_SECRET_KEY }}" ]; then - # Show first and last 4 characters of the secret key - SECRET_KEY="${{ secrets.SOROBAN_SECRET_KEY }}" - FIRST_CHARS=${SECRET_KEY:0:4} - LAST_CHARS=${SECRET_KEY: -4} - echo "SOROBAN_SECRET_KEY is set (length: ${#SECRET_KEY})" - echo "First 4 chars: $FIRST_CHARS" - echo "Last 4 chars: $LAST_CHARS" - else - echo "SOROBAN_SECRET_KEY is not set!" - fi - - if [ -n "${{ secrets.CONTRACT_ID }}" ]; then - # Show first and last 4 characters of the contract ID - CONTRACT_ID="${{ secrets.CONTRACT_ID }}" - FIRST_CHARS=${CONTRACT_ID:0:4} - LAST_CHARS=${CONTRACT_ID: -4} - echo "CONTRACT_ID is set (length: ${#CONTRACT_ID})" - echo "First 4 chars: $FIRST_CHARS" - echo "Last 4 chars: $LAST_CHARS" - else - echo "CONTRACT_ID is not set (this is normal for first deployment)" - fi - - - name: Deploy/Upgrade contract - env: - STELLAR_NETWORK: ${{ github.event.inputs.network || 'testnet' }} - STELLAR_ACCOUNT: ${{ github.event.inputs.account || 'alice' }} - STELLAR_SECRET_KEY: ${{ secrets.STELLAR_SECRET_KEY }} - SOROBAN_SECRET_KEY: ${{ secrets.SOROBAN_SECRET_KEY }} - CONTRACT_ID: ${{ secrets.CONTRACT_ID }} - run: | - echo "Deploying/Upgrading contract on $STELLAR_NETWORK using account $STELLAR_SECRET_KEY" - - # Make the script executable - chmod +x ./deploy_and_upgrade.sh - - # Create .stellar directory if it doesn't exist - mkdir -p .stellar - - # If CONTRACT_ID secret is available, save it to the file - if [ -n "$CONTRACT_ID" ]; then - echo "Using contract ID from GitHub secrets" - echo $CONTRACT_ID > .stellar/contract_id_${STELLAR_NETWORK}.txt - fi - - # Log the command that will be executed (without the actual secret) - echo "Executing: ./deploy_and_upgrade.sh deploy $STELLAR_NETWORK [SECRET_KEY]" - - # Run the deploy and upgrade script with upgrade command by default - ./deploy_and_upgrade.sh upgrade $STELLAR_NETWORK $STELLAR_SECRET_KEY - - # Check if the contract ID file was created - if [ -f .stellar/contract_id_${STELLAR_NETWORK}.txt ]; then - NEW_CONTRACT_ID=$(cat .stellar/contract_id_${STELLAR_NETWORK}.txt) - echo "Contract ID from file: $NEW_CONTRACT_ID" - echo "CONTRACT_ID=$NEW_CONTRACT_ID" >> $GITHUB_ENV - else - echo "Contract ID file not found. Deployment may have failed." - exit 1 - fi - - - name: Notify on success - if: success() - run: | - echo "Contract deployment/upgrade completed successfully!" - echo "Contract ID: $CONTRACT_ID" - echo "Network: $STELLAR_NETWORK" - echo "" - echo "IMPORTANT: Please update the CONTRACT_ID secret in your GitHub repository settings with the value above." - echo "You can do this by going to: Settings > Secrets and variables > Actions > New repository secret" - echo "Name: CONTRACT_ID" - echo "Value: $CONTRACT_ID" diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml index e94e67f..764263c 100644 --- a/.github/workflows/rustfmt.yml +++ b/.github/workflows/rustfmt.yml @@ -5,19 +5,12 @@ on: branches: - main - develop + - testnet pull_request: branches: - main - develop - testnet - workflow_call: - secrets: - STELLAR_SECRET_KEY: - required: true - SOROBAN_SECRET_KEY: - required: true - CONTRACT_ID: - required: false permissions: contents: read @@ -41,14 +34,3 @@ jobs: - name: Run rustfmt run: cargo fmt -- --check - - deploy: - needs: rustfmt - # Never deploy from a PR run. Deploy only on direct pushes (the workflow - # is chained here for the push-to-branch CD path, not for PR checks). - if: github.event_name == 'push' - uses: ./.github/workflows/contract-deploy.yml - secrets: - STELLAR_SECRET_KEY: ${{ secrets.STELLAR_SECRET_KEY }} - SOROBAN_SECRET_KEY: ${{ secrets.SOROBAN_SECRET_KEY }} - CONTRACT_ID: ${{ secrets.CONTRACT_ID }} diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index a8a0215..ecc4a11 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -4,6 +4,7 @@ on: branches: - main - develop + - testnet paths: - 'contracts/**' - '.github/workflows/verify-build.yml' @@ -57,14 +58,3 @@ jobs: - name: Run tests run: cargo test --release - - rustfmt: - needs: build - # On PRs the standalone rustfmt workflow posts the `rustfmt` check; this - # chained call (which fans out to deploy) runs only on push, never on a PR. - if: success() && github.event_name == 'push' - uses: ./.github/workflows/rustfmt.yml - secrets: - STELLAR_SECRET_KEY: ${{ secrets.STELLAR_SECRET_KEY }} - SOROBAN_SECRET_KEY: ${{ secrets.SOROBAN_SECRET_KEY }} - CONTRACT_ID: ${{ secrets.CONTRACT_ID }} diff --git a/README.md b/README.md index e37675e..b5c0a1f 100644 --- a/README.md +++ b/README.md @@ -8,24 +8,28 @@ Two contracts in one workspace. | Contract | Path | Purpose | |----------|------|---------| -| `boundless-events` | `contracts/events` | Event records (hackathon, bounty, grant) and inlined escrow. Multi-token whitelist. Idempotency. Per-pillar dispatch on a single canonical `create_event`. | -| `boundless-profile` | `contracts/profile` | Per-user credits and reputation. Lazy bootstrap. Per-token earnings tracking. Mutated almost exclusively by the events contract. | +| `boundless-events` | `contracts/events` | Event records for the four pillars (hackathon, bounty, grant, crowdfunding) with inlined escrow. Multi-token whitelist. Idempotent operations. Per-pillar dispatch on a single canonical `create_event`. Paged cancellation and timelocked upgrades. | +| `boundless-profile` | `contracts/profile` | Per-user reputation and per-token earnings. Lazy profile bootstrap. Mutated almost exclusively by the events contract. | -Specs live in the platform PRD set under the parent `boundless-repos/` directory: +## Deployments -- `boundless-platform-contract-prd.md` (events contract) -- `boundless-credits-reputation-prd.md` (profile contract) -- `boundless-chain-abstraction-adr.md` (off-chain readiness for a future second chain) -- `boundless-organizer-end-to-end-prd.md` (umbrella; everything organizer-side) +| Network | Contract | Address | Version | +|---------|----------|---------|---------| +| Mainnet | `boundless-events` | `CCFVEGOQJEM47LRAJU2LHEK4KTL5VYN7AOGZ2HH2GNHAMXTILNMMJGQZ` | `1.1.0` | +| Mainnet | `boundless-profile` | `CD3KH4OE7HDHHHUYFX3U4L7NLIILMXAY6HM5FEH2UH6UBOKX4HDNE3PC` | `1.1.0` | +| Testnet | `boundless-events` | `CBEODVJGUYCIYTVXD7KI5UG3BJ2UE4T7AGI2TGY3T4Q5GQRFGTRYVTZP` | `1.0.0` | +| Testnet | `boundless-profile` | `CCA3OAIBOZBUPHPRI5GI6N5PDTE7RTNLKAEID4JTC2YZIHIZNDX5Q6T3` | `1.0.0` | + +Both contracts expose an on-chain `version()` view; the versions above are live reads. ## Architecture in one paragraph -The events contract is the on-chain anchor of event existence, key terms, escrow custody, submission anchors, and winner records. The profile contract is the on-chain anchor of credit balances and reputation scores. The events contract calls into the profile contract for credit charging on apply, credit earning on accept, and reputation bumping on win. The off-chain orchestrator (`boundless-nestjs`) handles everything else: drafts, rich content via `content_uri`, KYC, role policies, AI features, moderation. The contracts hold the things the platform cannot afford to be trusted on; nestjs holds the things that benefit from iteration speed. +The events contract is the on-chain anchor of event existence, key terms, escrow custody, submission anchors, and winner records. Funds enter escrow at creation (or through top-ups), fees are taken at deposit, and payouts release through `select_winners` or per-milestone claims. The profile contract is the on-chain anchor of reputation scores and per-token earnings. The events contract calls into the profile contract to bootstrap profiles on first touch, bump reputation on wins and milestones, and register earnings on payout. Every state-mutating operation carries an idempotency key, admin surfaces are pausable, and upgrades are timelocked. ## Prerequisites - Rust 1.90.0 (`rust-toolchain.toml` pins this). -- Soroban SDK 23.5.2. +- Soroban SDK 23.5.x (workspace-pinned in `Cargo.toml`). - `stellar` CLI for building WASM (`brew install stellar/tap/stellar-cli`). - `wasm32v1-none` target: `rustup target add wasm32v1-none`. @@ -44,14 +48,17 @@ cd contracts/events && make size && cd ../.. cd contracts/profile && make size && cd ../.. ``` +Test snapshots under `contracts/*/test_snapshots/` are the audit trail. When fixtures change shape, regenerate snapshots intentionally and commit them in the same PR. + ## Repo layout ``` boundless-contract/ ├── Cargo.toml # workspace ├── rust-toolchain.toml -├── .cargo/ -├── .github/workflows/ # verify, deploy, rustfmt +├── deploy_and_upgrade.sh # testnet deploy / upgrade helper +├── deploy_mainnet.sh # mainnet cold deploy +├── .github/workflows/ # verify-build, rustfmt, deploy ├── contracts/ │ ├── events/ │ │ ├── Cargo.toml @@ -62,13 +69,15 @@ boundless-contract/ │ │ ├── errors.rs # error code enum │ │ ├── events.rs # contract event emissions │ │ ├── storage.rs # persistent / temporary key helpers -│ │ ├── admin.rs # init, rotation, pause, upgrade +│ │ ├── admin.rs # init, rotation, pause, timelocked upgrade │ │ ├── token_whitelist.rs # admin-managed supported-tokens set │ │ ├── escrow.rs # fee math, token transfer helpers │ │ ├── event_ops.rs # canonical create / submit / select_winners │ │ ├── hackathon.rs # pillar-specific validation │ │ ├── bounty.rs # pillar-specific validation + apply / withdraw │ │ ├── grant.rs # pillar-specific validation + claim_milestone +│ │ ├── crowdfunding.rs # pillar-specific validation +│ │ ├── profile_client.rs # cross-contract interface to boundless-profile │ │ ├── idempotency.rs # OpSeen helpers + deployment-epoch ID base │ │ └── tests/ # per-area integration tests │ └── profile/ @@ -76,34 +85,31 @@ boundless-contract/ │ ├── Makefile │ └── src/ │ ├── lib.rs # contract entry, public surface -│ ├── types.rs # Profile, PendingAdmin +│ ├── types.rs # Profile, PendingAdmin, PendingUpgrade │ ├── errors.rs │ ├── events.rs │ ├── storage.rs -│ ├── admin.rs # init, two-step rotations, pause, upgrade -│ ├── credits.rs # bootstrap, spend, earn, refund, admin_grant +│ ├── admin.rs # init, two-step rotations, pause, timelocked upgrade +│ ├── bootstrap.rs # lazy profile creation (bootstrap / bootstrap_self) │ ├── reputation.rs # bump, slash, admin_slash │ ├── earnings.rs # per-token earnings registration │ ├── idempotency.rs # OpSeen helpers │ └── tests/ -└── docs/ - └── ARCHITECTURE.md # the why +└── scripts/ + ├── admin/ # verify-multisig.sh + └── deploy/ # deploy.sh, register_token.sh ``` -## Deployment - -Two-step sequence (see `boundless-credits-reputation-prd.md` Section 13.1): - -1. Deploy `boundless-profile` with `default_bootstrap_credits = 10`. -2. Deploy `boundless-events` with the profile contract's address + admin + fee_account + fee_bps. -3. Call `profile.set_events_contract(events_addr)`, then `accept_events_contract` as the events contract address. -4. Register supported tokens on `boundless-events` (USDC at launch). +## Deployment and upgrades -Deployment scripts live in `scripts/`. Mainnet deploys are multisig-gated and only after external audit clears. +Fresh deploy sequence for a new network: -## Audit +1. Deploy `boundless-profile` with the admin address. +2. Deploy `boundless-events` with the profile contract's address, admin, fee account, and fee bps. +3. Call `profile.set_events_contract(events_addr)` (first-set-only; later rotation is a timelocked two-step). +4. Register supported tokens on `boundless-events`. -External audit is the hard gate before mainnet. Audit checklist is in the platform contract PRD Section 15.3. +Upgrades are timelocked three-step operations: `propose_upgrade(wasm_hash, version)`, wait the timelock (~1 day on mainnet), `apply_upgrade()`, then `migrate()` to stamp the migration marker. A pending proposal can be inspected with `get_pending_upgrade()` and withdrawn with `cancel_pending_upgrade()`. ## Contributing diff --git a/deploy_and_upgrade.sh b/deploy_and_upgrade.sh index e2ac9c7..edd9bd9 100755 --- a/deploy_and_upgrade.sh +++ b/deploy_and_upgrade.sh @@ -98,7 +98,7 @@ deploy_contract() { echo -e "${YELLOW}NOTE: This script does NOT pass constructor args.${NC}" echo "After the deploy succeeds, invoke the constructor manually per the runbook:" echo " docs/mainnet-deploy-runbook.md (events: admin, fee_account, fee_bps, profile_contract)" - echo " docs/mainnet-deploy-runbook.md (profile: admin, default_bootstrap_credits)" + echo " docs/mainnet-deploy-runbook.md (profile: admin)" echo "" CONTRACT_ID=$(stellar contract deploy \ diff --git a/deploy_mainnet.sh b/deploy_mainnet.sh index e73795c..3f4f372 100755 --- a/deploy_mainnet.sh +++ b/deploy_mainnet.sh @@ -115,7 +115,6 @@ cmd_build_release() { cmd_deploy_profile() { require_env INITIAL_ADMIN_KEY - require_env BOOTSTRAP_PROFILE_CREDITS require_cli confirm_mainnet ensure_deployments_dir @@ -136,8 +135,7 @@ cmd_deploy_profile() { --source "$INITIAL_ADMIN_KEY" \ --wasm "$wasm" \ -- \ - --admin "$(stellar keys address "$INITIAL_ADMIN_KEY")" \ - --default_bootstrap_credits "$BOOTSTRAP_PROFILE_CREDITS") + --admin "$(stellar keys address "$INITIAL_ADMIN_KEY")") ok "profile_contract=$profile_id" deployment_set profile_contract "$profile_id" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8d4557f..dfa9806 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -4,23 +4,24 @@ There are two coherent ways to build a contract for a platform like Boundless: -**Custody-only on-chain.** Contract holds money. Everything else (event records, submissions, winners, credits, reputation) lives off-chain in Postgres. The platform is the source of truth. Cheap to audit; useless for transparency; reintroduces all the trust we're trying to remove. +**Custody-only on-chain.** Contract holds money. Everything else (event records, submissions, winners, reputation) lives off-chain in Postgres. The platform is the source of truth. Cheap to audit; useless for transparency; reintroduces all the trust we're trying to remove. -**Custody plus the records that matter on-chain.** Contract holds money, event existence, key terms, submission anchors, winner records, credits, and reputation. The platform is the source of truth for everything that benefits from iteration speed (descriptions, KYC, role policies, AI) but cannot edit the things that demand transparency. +**Custody plus the records that matter on-chain.** Contract holds money, event existence, key terms, submission anchors, winner records, and reputation. The platform is the source of truth for everything that benefits from iteration speed (descriptions, KYC, role policies, AI) but cannot edit the things that demand transparency. We took the second path. This document explains the boundary. +The boundary has moved once: through contract version 1.0.0, per-user credit balances also lived on-chain in the profile contract. The 1.0.0 → 1.1.0 upgrade (2026-06) removed them. Credits are a product-pricing lever, not a settlement promise — they were being retuned at product speed, which is exactly the iteration-speed test above. They now live in an off-chain ledger in boundless-nestjs; the profile contract holds only reputation scores and per-token earnings. + ## What goes on-chain | Family | Lives in | Why | |--------|----------|-----| | Event existence (id, owner, pillar, token, total_budget) | events contract | Organizers cannot quietly delete events that went badly. | -| Event key terms (release_kind, deadline, winner_distribution, application_credit_cost) | events contract | These define what is owed to whom; mutating them off-chain would let the platform rewrite outcomes. | +| Event key terms (release_kind, deadline, winner_distribution) | events contract | These define what is owed to whom; mutating them off-chain would let the platform rewrite outcomes. | | Submission anchors (applicant, content_uri, timestamp) | events contract | Tamper-proof participation history. | | Winner records (recipient, position, amount, milestone, paid_at) | events contract | The promise of "you won this much" has to be enforced by the chain. | | Escrow balance and fee withholding | events contract | The platform cannot hold the keys to event funds. | | Token whitelist | events contract | Admin-managed; on-chain so the whitelist is auditable. | -| Credit balances | profile contract | The platform cannot mint or freeze credits. | | Reputation scores | profile contract | The platform cannot inflate or deflate reputation. | | Per-token earnings | profile contract | The promise of "you've earned this much" matches the chain's record of releases. | | Idempotency markers | both contracts (temporary storage with TTL) | Replay-safe state transitions across the orchestrator's retry surface. | @@ -33,6 +34,7 @@ We took the second path. This document explains the boundary. | Draft state | Drafts never hit the contract. Only published events do. | | KYC tiers and verification details | PII. | | Role policies and per-event reviewer assignments | Iterate rapidly without contract upgrades. | +| Credit balances and credit charges (application costs, refunds) | Off-chain ledger since the 1.1.0 upgrade (2026-06). Credits are pricing policy, not settlement; keeping them on-chain froze product tuning behind contract upgrades. | | Tier brackets and credit-discount rules | Off-chain policy that interprets on-chain scores; lets product tune without audit. | | AI features (organizer assist, judging assist, similarity detection) | Out of scope for any contract. | | Moderation queue, notifications, analytics, exports | None of these benefit from immutability. | @@ -40,20 +42,21 @@ We took the second path. This document explains the boundary. ## Why two contracts and not one -Soroban's compiled WASM size limit is 64 KB. The platform contract that holds events + escrow + multi-token + idempotency lands around 50 KB once all operation bodies are implemented. Adding credits and reputation operations would push it over. +Soroban's compiled WASM size limit is 64 KB. The platform contract that holds events + escrow + multi-token + idempotency lands around 50 KB once all operation bodies are implemented. Adding reputation and earnings operations would push it over. -The natural split line: events are per-event; profile is per-user. Cross-family operations (apply charges credits; select_winners bumps reputation) happen at the events-to-profile boundary, where the events contract calls the profile contract with explicit auth. +The natural split line: events are per-event; profile is per-user. Cross-family operations (apply bootstraps the applicant's profile; select_winners bumps reputation and registers earnings) happen at the events-to-profile boundary, where the events contract calls the profile contract with explicit auth. Three contracts would have been over-engineering. Two contracts split is the smallest cut that keeps both under the ceiling while keeping cross-contract calls bounded. ## The cross-contract dance ``` -applicant.require_auth() in events contract +event owner require_auth() in events contract (select_winners) events contract holds the binding for the profile contract address - events contract calls profile.spend_credits(applicant, cost, "apply", op_id) + events contract calls profile.bump_reputation(winner, delta, "win", child_op_id) + events contract calls profile.register_earnings(winner, token, amount, child_op_id) profile contract verifies events_contract.require_auth() - profile contract checks balance, debits, emits CreditsSpent + profile contract mutates reputation / earnings, emits events ``` Users cannot call profile mutations directly. The profile contract verifies the caller is the registered events contract address (or admin for direct grants and slashes). The `events_contract` binding in the profile contract is two-step rotatable so the events contract can be upgraded or replaced without re-deploying profile. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 8db7fd0..f91114e 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -19,9 +19,10 @@ Pick once per environment and document. | **Admin identity** | both contracts' `admin` | controls `set_admin`, `set_fee_bps`, `pause`, `upgrade`, `register_supported_token`. **Use a Stellar multisig account for mainnet.** Testnet can be a single key. | | **Fee account** | events `fee_account` | G-address that receives the platform fee on every deposit. Must hold a trustline for every registered token. **Should be a separately-keyed account from admin.** | | **Fee bps** | events `fee_bps` | Basis points. 100 = 1%, 250 = 2.5%. Contract caps at 1000 (10%) per audit L4. | -| **Bootstrap credits** | profile `default_bootstrap_credits` | u32. Initial credit balance for newly-created profiles. PRD default is 10. | | **USDC asset address** | events `register_supported_token` | Token contract (SAC) address. See Section 4. | +> Contract versions up to 1.0.0 also took a `default_bootstrap_credits` constructor parameter on the profile contract. The 1.1.0 upgrade (2026-06) removed on-chain credits — they are now an off-chain ledger in boundless-nestjs — so the profile constructor takes only `admin`. + Production values land in `deployments/.json` after `deploy.sh` runs. --- @@ -83,7 +84,6 @@ Required values: ADMIN_IDENTITY=boundless-admin FEE_ACCOUNT=G... # output of `stellar keys address boundless-fee` FEE_BPS=250 # 2.5% -BOOTSTRAP_CREDITS=10 # PRD default ``` `.env.deploy` is gitignored. Do not commit. @@ -271,5 +271,5 @@ Or, more cleanly, write a follow-up migration that drops the table. - `scripts/deploy/deploy.sh`, `register_token.sh`, `verify.sh` - `boundless-platform-contract-prd.md` Section 12 (deployment) - `boundless-payout-prd.md` Section 9.1 (orchestrator integration) -- `boundless-credits-reputation-prd.md` Section 9 (bootstrap credit policy) +- `boundless-credits-reputation-prd.md` Section 9 (credit policy — implemented as an off-chain ledger in boundless-nestjs since the 1.1.0 upgrade) - `.env.deploy.example` (per-network parameter template) diff --git a/docs/audit-2026-06-stellar-skill.md b/docs/audit-2026-06-stellar-skill.md index 5019358..72b839d 100644 --- a/docs/audit-2026-06-stellar-skill.md +++ b/docs/audit-2026-06-stellar-skill.md @@ -8,6 +8,15 @@ Part 5 (Common Pitfalls). Scope: `contracts/events` and `contracts/profile` source. Tests, deploy scripts, and off-chain runbooks are out of scope here; they get their own pass. +> **Historical note (post-audit):** this report describes the contracts as +> audited, at version 1.0.0, when the profile contract still held per-user +> credit balances on-chain (`profile/src/credits.rs`, the `credits` field on +> `Profile`, `application_credit_cost` on events). The 1.0.0 → 1.1.0 upgrade +> (2026-06) removed on-chain credits; they are now an off-chain ledger in +> boundless-nestjs, and the profile contract holds only reputation scores and +> per-token earnings. Credit-related references below are accurate for the +> code as audited and are preserved unedited. + ## TL;DR Boundless ships solid Soroban hygiene: typed errors, typed `DataKey` keys, diff --git a/docs/contract-ops-runbook.md b/docs/contract-ops-runbook.md index f76fd3c..2d1ec67 100644 --- a/docs/contract-ops-runbook.md +++ b/docs/contract-ops-runbook.md @@ -81,11 +81,10 @@ PROFILE_ID=$(stellar contract deploy \ --wasm target/wasm32v1-none/release/boundless_profile.wasm \ --source boundless-deployer --network testnet \ -- \ - --admin "$DEPLOYER" \ - --default_bootstrap_credits 10) + --admin "$DEPLOYER") echo "PROFILE_ID=$PROFILE_ID" # WRITE THIS DOWN ‹drill: CA63ATN2…› ``` -> Constructor arg is **`--default_bootstrap_credits`** (not `--bootstrap_credits`). See Section 7. +> Since the 1.1.0 credit-removal upgrade (2026-06) the profile constructor takes only `--admin`. Contract 1.0.0 additionally required `--default_bootstrap_credits`; credits now live in an off-chain ledger in boundless-nestjs. See Section 7. ### 4.3 Deploy the events contract ```bash @@ -236,7 +235,7 @@ curl -s "https://horizon-testnet.stellar.org/accounts/" | jq '{signers,thr ## 7. Pre-mainnet code fixes (found during the drill) -1. **`deploy_mainnet.sh` deploy-profile uses `--bootstrap_credits`**, but the profile constructor arg is **`--default_bootstrap_credits`**. As-is, the mainnet profile deploy would fail. Fix the flag in the script. +1. ~~**`deploy_mainnet.sh` deploy-profile uses `--bootstrap_credits`**, but the profile constructor arg is **`--default_bootstrap_credits`**.~~ Overtaken by the 1.1.0 credit-removal upgrade (2026-06): the profile constructor no longer takes any credits argument (credits are an off-chain ledger in boundless-nestjs). Deploy scripts must pass only `--admin` when deploying profile from current source. 2. **`INITIAL_VERSION` is still `0.2.0`** while the code now includes the supported-token enumeration. A fresh mainnet deploy would stamp `0.2.0` for a contract that differs from the audited `0.2.0` surface. Bump it (e.g. `1.0.0`) and update the upgrade-test fixtures + the runbook's expected-version checks. 3. **Re-audit the supported-token enumeration** — it's new contract code added after the last audit; the mainnet pre-flight gate ("all critical/high resolved") must cover it. diff --git a/docs/mainnet-deploy-runbook.md b/docs/mainnet-deploy-runbook.md index 6e57ca8..e88c682 100644 --- a/docs/mainnet-deploy-runbook.md +++ b/docs/mainnet-deploy-runbook.md @@ -71,7 +71,9 @@ export MULTISIG_THRESHOLD=2 # 2-of-3 to start; review at scale # Boundless platform constants export INITIAL_GLOBAL_FEE_BPS=250 # 2.5% Bounty default; overridden per event -export BOOTSTRAP_PROFILE_CREDITS=10 +# (Contract 1.0.0 also required BOOTSTRAP_PROFILE_CREDITS here; the 1.1.0 +# credit-removal upgrade moved credits to an off-chain ledger in +# boundless-nestjs, and the profile constructor now takes only --admin.) # USDC SAC address on Stellar mainnet (verify against Circle's documentation # at the time of deploy; this is the canonical Stellar Asset Contract for USDC). @@ -92,8 +94,7 @@ PROFILE_ID=$(soroban contract deploy \ --source $INITIAL_ADMIN_KEY \ --wasm $PROFILE_WASM \ -- \ - --admin $(soroban keys address $INITIAL_ADMIN_KEY) \ - --default_bootstrap_credits $BOOTSTRAP_PROFILE_CREDITS) + --admin $(soroban keys address $INITIAL_ADMIN_KEY)) echo "PROFILE_ID=$PROFILE_ID" | tee -a deployments/mainnet.env ``` diff --git a/docs/multisig-guide.md b/docs/multisig-guide.md index c9fd6d6..08cbf5d 100644 --- a/docs/multisig-guide.md +++ b/docs/multisig-guide.md @@ -217,7 +217,8 @@ Open `.env.deploy` in your editor and set: | `ADMIN_IDENTITY` | The name of the Stellar CLI identity that will deploy and hold initial admin authority. Default is `boundless-admin`. This is the throwaway deployer per §E.0, NOT the final admin. Rename to `boundless-deployer` if the default name reads as misleading; the scripts only care about the value. | | `FEE_ACCOUNT` | The G-address that receives platform fees. **Separate account.** Not the deployer, not the multi-sig. Must hold a trustline for every token you register; the contract does not enforce this at registration. | | `FEE_BPS` | Platform fee in basis points (`100 = 1%`, `250 = 2.5%`). The `.env.deploy.example` template and `deploy.sh` validate the range as `[0, 1000]`, matching the contract's `MAX_FEE_BPS = 1000` (10%) cap per the 2026-06 audit (L4 finding). Values above 1000 are rejected at the script level before any deploy work happens. | -| `BOOTSTRAP_CREDITS` | Starting credit balance assigned to newly-bootstrapped profiles. Default is 10 per the credits / reputation PRD. | + +> Contract versions up to 1.0.0 also required a `BOOTSTRAP_CREDITS` value for the profile constructor. The 1.1.0 credit-removal upgrade (2026-06) moved credits to an off-chain ledger in boundless-nestjs; the profile constructor now takes only `--admin`. `.env.deploy` is gitignored. Never commit a populated file. @@ -268,7 +269,7 @@ The script `scripts/deploy/deploy.sh` does the whole deploy in one shot: builds The script: - Aborts if `stellar` CLI is older than 26.0.0 (Rust 1.90.0 emits reference-types wasm that earlier CLIs reject at simulation time). -- Aborts if any of `ADMIN_IDENTITY`, `FEE_ACCOUNT`, `FEE_BPS`, `BOOTSTRAP_CREDITS` is unset. +- Aborts if any of `ADMIN_IDENTITY`, `FEE_ACCOUNT`, `FEE_BPS` is unset. - Aborts if `FEE_BPS` is outside `[0, 1000]` (matches the contract's `MAX_FEE_BPS = 1000` cap per audit L4; see Step 1). - Prints the profile contract id and events contract id on success. @@ -281,7 +282,7 @@ BOUNDLESS_PROFILE_CONTRACT_ADDRESS= Under the hood, the constructor invocations are: -- `boundless-profile`: `--admin --default_bootstrap_credits `. +- `boundless-profile`: `--admin `. - `boundless-events`: `--admin --fee_account --fee_bps --profile_contract `. - Wiring call: `profile.set_events_contract --new_addr `. @@ -319,7 +320,7 @@ Repeat for each token you support (USDC mandatory, native XLM SAC if you accept The script reads `deployments/mainnet.json` and queries each contract for its full admin-visible state: - **Events:** `get_admin`, `get_fee_account`, `get_fee_bps`, `get_profile_contract`, `is_paused`. -- **Profile:** `get_admin`, `get_events_contract`, `get_default_bootstrap_credits`, `is_paused`. +- **Profile:** `get_admin`, `get_events_contract`, `is_paused`. Expected output at this point: @@ -328,7 +329,6 @@ Expected output at this point: - `events.fee_bps` matches `.env.deploy`. - `events.profile_contract` matches the profile id. - `profile.events_contract` matches the events id. -- `profile.default_bootstrap_credits` matches `.env.deploy`. - Both `is_paused` return `false`. If anything is wrong, fix it now. The deployer still has single-sig admin authority through Step 8; from Step 9 onward every fix needs two signatures. @@ -465,7 +465,7 @@ After Step 12, the contracts are live, the multi-sig is admin on both, and there #### Recap of the whole sequence -1. Configure `.env.deploy` (ADMIN_IDENTITY, FEE_ACCOUNT, FEE_BPS, BOOTSTRAP_CREDITS). +1. Configure `.env.deploy` (ADMIN_IDENTITY, FEE_ACCOUNT, FEE_BPS). 2. Generate the deployer identity (`stellar keys generate boundless-deployer`). 3. Fund the deployer (~10 XLM). 4. Run `./scripts/deploy/deploy.sh mainnet` (builds + deploys both contracts + wires `set_events_contract` + writes `deployments/mainnet.json`). diff --git a/docs/scout-audit-report.md b/docs/scout-audit-report.md index 6c6bc49..b7a922b 100644 --- a/docs/scout-audit-report.md +++ b/docs/scout-audit-report.md @@ -6,6 +6,14 @@ **Scope:** `contracts/events` + `contracts/profile` **Command:** `cargo scout-audit` (run from workspace root) +> **Historical note (post-scan):** this report describes the contracts as +> scanned, at version 1.0.0, when per-user credit balances were still +> on-chain (`profile/src/credits.rs`). The 1.0.0 → 1.1.0 upgrade (2026-06) +> removed on-chain credits — they are now an off-chain ledger in +> boundless-nestjs — so `credits.rs` and its findings (e.g. C-1 / C-6) no +> longer have an on-chain counterpart. References below are preserved +> unedited as the record of the scan. + --- ## Summary diff --git a/docs/threat-model.md b/docs/threat-model.md index 4667e35..fe38ccf 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -1,7 +1,7 @@ # Boundless Platform — STRIDE Threat Model -**Version:** 1.1 -**Date:** June 2026 (rev. June 2026 after second Scout remediation) +**Version:** 1.2 +**Date:** July 2026 (rev. after the 1.1.0 credit-removal upgrade; prior rev. June 2026 after second Scout remediation) **Scope:** `boundless-events` + `boundless-profile` Soroban contracts and the off-chain orchestrator (`boundless-nestjs`) **Prepared by:** Boundless Engineering **Status:** Submitted for SDF Soroban Security Audit Program @@ -19,7 +19,7 @@ The system consists of two Soroban smart contracts and an off-chain orchestrator | Component | Role | |---|---| | `boundless-events` | On-chain anchor for all event types. Manages escrow custody (deposit, lock, release, refund), event lifecycle, participant registration, submission anchoring, and winner selection for all four pillars (hackathon, bounty, grant, crowdfunding). | -| `boundless-profile` | Per-user credits and reputation scores. Called cross-contract by `boundless-events` to charge credits on application and award reputation on win. | +| `boundless-profile` | Per-user reputation scores and per-token earnings. Called cross-contract by `boundless-events` to bootstrap profiles on application, award reputation on win, and register earnings on payout. (Credit balances were on-chain here through contract 1.0.0; the 1.1.0 upgrade moved them to an off-chain ledger in `boundless-nestjs`.) | | `boundless-nestjs` | Off-chain orchestrator (NestJS). Holds rich content, user accounts, KYC state, and draft lifecycle. Builds and submits Stellar transactions via RPC. Houses the admin KYC workflow and the server-side admin co-signer. | | Frontend (`boundless`) | Next.js web app. Runs in the user's browser. Calls the backend API. No direct RPC calls; all chain interaction is mediated by the backend. | | Admin Portal | Staff-only Next.js app. Calls the backend admin API surface. Enforces step-up TOTP for sensitive mutations. | @@ -69,8 +69,8 @@ The system consists of two Soroban smart contracts and an off-chain orchestrator ▼ ▼ ┌──────────────────┐ ┌────────────────────┐ │ boundless-events │──cross-contract│ boundless-profile │ - │ (escrow, event │───────────────▶│ (credits + │ - │ lifecycle for │ │ reputation) │ + │ (escrow, event │───────────────▶│ (reputation + │ + │ lifecycle for │ │ earnings) │ │ all 4 pillars) │ └────────────────────┘ └──────────────────┘ │ @@ -108,7 +108,7 @@ The system consists of two Soroban smart contracts and an off-chain orchestrator | Store | Location | Contents | Sensitivity | |---|---|---|---| | PostgreSQL | Boundless servers | User accounts, event drafts, escrow op log, KYC status (no PII), audit log | Medium — no documents or keys | -| Soroban contract storage | Stellar ledger | Event records, escrow balances, winner assignments, credits, reputation | Public by design — pseudonymous addresses only | +| Soroban contract storage | Stellar ledger | Event records, escrow balances, winner assignments, reputation scores, per-token earnings | Public by design — pseudonymous addresses only | | Didit systems | Didit infrastructure | Identity documents, biometrics, verification decisions | High — held exclusively by Didit | | Admin multi-sig keys | Distributed (3 signers, separate machines) | Freighter wallet keys (software baseline) | Critical | | Backend environment | Server environment vars | JWT secret, encryption key, Didit API key, orchestrator signing key | High | @@ -152,7 +152,7 @@ The system consists of two Soroban smart contracts and an off-chain orchestrator | Tamp.4 | A malicious or compromised Stellar RPC node returns forged event data to the backend indexer, causing the platform to display incorrect state. | Backend event indexer, Nodies RPC | | Tamp.5 | The WASM hash in a `propose_upgrade` call is quietly replaced between proposal and application to install malicious contract logic. | Contract upgrade flow (`admin.rs`) | | Tamp.6 | An attacker intercepts and modifies a partially-signed multi-sig transaction in transit to change the operation parameters (e.g., new admin address). | Admin multi-sig signing workflow | -| Tamp.7 | An integer underflow in `spend_credits` allows a builder to spend more credits than their balance, wrapping the counter and corrupting credits accounting across the platform. | `boundless-profile` contract, `credits.rs` | +| Tamp.7 | *(retired in 1.1.0)* An integer underflow in `spend_credits` allows a builder to spend more credits than their balance, wrapping the counter and corrupting credits accounting across the platform. | `boundless-profile` contract 1.0.0, `credits.rs` | | Tamp.8 | An integer underflow in `remove_applicant` corrupts the applicant index (slot or count wraps to u32::MAX), causing subsequent lookups to find phantom applicants or block legitimate removals. | `boundless-events` contract, `storage.rs` | #### Repudiation @@ -195,7 +195,7 @@ The system consists of two Soroban smart contracts and an off-chain orchestrator | EoP.2 | A staff member with `kyc:read` (Tier 0) permission calls the `kyc:override` endpoint (Tier 2) to approve their own or a collaborator's KYC. | Backend admin KYC controller, `PolicyGuard` | | EoP.3 | An organizer calls `claim_milestone` on a crowdfunding event without the required admin co-authorization, attempting to claim funds early. | `boundless-events` contract, admin.require_auth() | | EoP.4 | A regular backend API user (builder/organizer) sends requests to the `/admin/v2/*` routes by crafting bearer tokens with elevated claims. | Backend admin API, `StaffAuthGuard` | -| EoP.5 | An attacker calls `bootstrap_self` on `boundless-profile` with another user's address to create a profile and claim their credits. | `boundless-profile` contract | +| EoP.5 | An attacker calls `bootstrap_self` on `boundless-profile` with another user's address to create or squat a profile record on their behalf. | `boundless-profile` contract | | EoP.6 | A malicious event owner calls `set_admin` or `propose_upgrade` directly, attempting to rotate the contract admin to an address they control. | `boundless-events` contract, `admin.rs` | | EoP.7 | The backend orchestrator key is used to approve a `claim_milestone` on behalf of an event where the organizer did not initiate the call, redirecting funds. | Backend orchestrator, `boundless-events` | @@ -218,7 +218,7 @@ The system consists of two Soroban smart contracts and an off-chain orchestrator | **Tamp.4** | The backend verifies event existence against the contract before acting (read-then-act). The Nodies RPC endpoint is managed and dedicated; the backend does not follow `_links` navigation. Soroban event payloads include the emitting contract ID; the indexer filters on the known contract address. | | **Tamp.5** | `propose_upgrade(wasm_hash, new_version)` stores the WASM hash on-chain. `apply_upgrade()` can only be called after the timelock elapses (~1 day) and re-verifies against the stored hash. The proposal is public on-chain; off-chain monitors can detect unexpected upgrade proposals. The admin multi-sig must authorize both steps. | | **Tamp.6** | Multi-sig transactions are built offline, inspected before signing, and signed independently by each signer. The `verify-multisig.sh` script validates the on-chain signer configuration. Future target state: hardware keys (Yubikey/Ledger) eliminate in-memory key exposure. | -| **Tamp.7** | `spend_credits` uses `profile.credits.checked_sub(amount).ok_or(Error::InsufficientCredits)?`. The `checked_sub` returns `None` on underflow; `ok_or` converts it to a typed contract error. The operation reverts atomically -- no partial state is written. The separate `if profile.credits < amount` guard was redundant and has been removed, leaving a single authoritative check. Fixed in Scout remediation round 2 (`profile/credits.rs`). | +| **Tamp.7** | Retired. In contract 1.0.0 this was mitigated with `profile.credits.checked_sub(amount).ok_or(Error::InsufficientCredits)?` (Scout remediation round 2). The 1.0.0 → 1.1.0 upgrade then removed on-chain credits entirely — `spend_credits` and `credits.rs` no longer exist, and credit accounting lives in the off-chain ledger in `boundless-nestjs`, where it is covered by that system's own controls. | | **Tamp.8** | `remove_applicant` now uses `slot.checked_sub(1).ok_or(Error::ApplicantNotApplied)?` and `count.checked_sub(1).ok_or(Error::EventNotFound)?` for all three arithmetic operations. Even though the `if slot == 0` guard above makes underflow structurally impossible, checked arithmetic makes the invariant explicit and contract-level: if the guard were ever removed or the code path changed, the checked operation would surface the error rather than silently wrapping. Fixed in Scout remediation round 2 (`events/storage.rs`). | | **Repud.1** | `select_winners` emits a `WinnersSelected` Soroban event containing the event ID, winner addresses, and amounts. This event is immutable on the Stellar ledger and publicly auditable on Stellar Expert. | | **Repud.2** | Every USDC release is an on-chain SAC `transfer` call. The transaction hash, sender, recipient, and amount are permanently recorded on the Stellar ledger. | @@ -231,7 +231,7 @@ The system consists of two Soroban smart contracts and an off-chain orchestrator | **Info.4** | Staff sessions use short-lived JWTs. Tier 2 actions require a fresh TOTP token. Admin workstations are on separate browser profiles from personal use. Future: hardware-backed staff keys. | | **Info.5** | The Didit webhook handler logs only the `session_id` and normalized status, never the full payload. Log levels are set to `error,warn,log` in production; raw request bodies are not logged. | | **Info.6** | The fee account is a Stellar G-address whose key is held by the admin multi-sig (same custody as the contract admin). Fee diversion requires compromising the multi-sig. | -| **DoS.1** | Per-event applicant cap is 5,000 entries. The `apply` endpoint requires a valid JWT and KYC-approved status, raising the cost of bot registration significantly. Credit costs on application (`application_credit_cost`) are configurable per event, making spam economically prohibitive for events with non-zero credit costs. | +| **DoS.1** | Per-event applicant cap is 5,000 entries. The `apply` endpoint requires a valid JWT and KYC-approved status, raising the cost of bot registration significantly. Per-event credit costs on application are enforced by the off-chain credit ledger at the API layer (since the 1.1.0 upgrade; previously on-chain via `application_credit_cost`), making spam economically prohibitive for events with non-zero credit costs. | | **DoS.2** | The paged cancel design (`start_cancel` / `process_cancel_batch(max_refunds=25)` / `finalize_cancel`) caps per-transaction refund work at 25 entries, well within Stellar ledger resource limits even at 5,000 contributors. | | **DoS.3** | `GlobalThrottlerGuard` applies per-IP rate limits globally. The backend runs behind a load balancer with DDoS protection at the infrastructure layer. | | **DoS.4** | Nodies is a managed, SLA-backed RPC provider. BullMQ queues buffer all async contract operations; if RPC is temporarily unavailable, jobs retry with exponential backoff and are not lost. The `DistributedLockService` prevents duplicate submissions during retry storms. | @@ -280,6 +280,7 @@ Four issues surfaced during or directly after the modelling process: 2. The audit log for KYC overrides did not originally include the `reason` field in the initial implementation. Reviewing the Repudiation category prompted us to confirm this was added and tested -- it is present in `AdminKycActionsService.override()`. 3. **(v1.1)** A second Scout static analysis run after this document was first published identified two previously-undocumented integer underflow vectors: `profile/credits.rs` (Tamp.7) and `events/storage.rs` (Tamp.8). Both were fixed by converting bare `- 1` and `-= amount` operations to `checked_sub` with typed error returns. These threats have been added to Section 2 and Section 3 in this revision. 4. **(v1.1)** The ENHANCEMENT category in the Scout report flagged ~50 profile contract functions for missing event emissions. Code review confirmed all state-mutating functions already emit events in their implementation modules; Scout cannot trace through function call chains. Documented as false positives. +5. **(v1.2)** The 1.0.0 → 1.1.0 contract upgrade (2026-06) removed per-user credit balances from the profile contract; credits now live in an off-chain ledger in `boundless-nestjs`. Component descriptions, the dataflow diagram, and the data-store table were updated; Tamp.7 was marked retired (its subject code, `credits.rs`, no longer exists on-chain); EoP.5 and DoS.1 were reworded to match the post-upgrade surface. Credit-ledger threats are now in scope of the backend's threat surface (TB1), not the contract's. ### Living Document Commitment diff --git a/scripts/deploy/deploy.sh b/scripts/deploy/deploy.sh index 2adfb71..9e90b9d 100755 --- a/scripts/deploy/deploy.sh +++ b/scripts/deploy/deploy.sh @@ -10,7 +10,10 @@ # ADMIN_IDENTITY stellar CLI identity name with admin authority # FEE_ACCOUNT Stellar G-address that receives fees # FEE_BPS platform fee in basis points (e.g. 250 = 2.5%) -# BOOTSTRAP_CREDITS starting credit balance for new profiles (e.g. 10) +# +# Contract 1.0.0 also required BOOTSTRAP_CREDITS; the 1.1.0 credit-removal +# upgrade (2026-06) moved credits off-chain and the profile constructor now +# takes only --admin. # # Spec: boundless-platform-contract-prd.md Section 12.2. @@ -44,7 +47,6 @@ require() { require ADMIN_IDENTITY require FEE_ACCOUNT require FEE_BPS -require BOOTSTRAP_CREDITS # Soroban host on Stellar testnet accepts WebAssembly emitted with the # reference-types extension; stellar-cli's local simulator only validates that @@ -86,7 +88,6 @@ echo " network: $NETWORK" echo " admin identity: $ADMIN_IDENTITY ($ADMIN_ADDR)" echo " fee account: $FEE_ACCOUNT" echo " fee bps: $FEE_BPS" -echo " bootstrap credits: $BOOTSTRAP_CREDITS" echo # 1. Build both contracts. Testnet/futurenet enable the `testnet` feature → @@ -117,8 +118,7 @@ PROFILE_ID=$(stellar contract deploy \ --source "$ADMIN_IDENTITY" \ --network "$NETWORK" \ -- \ - --admin "$ADMIN_ADDR" \ - --default_bootstrap_credits "$BOOTSTRAP_CREDITS") + --admin "$ADMIN_ADDR") echo " profile contract id: $PROFILE_ID" # 3. Deploy boundless-events pointing at the profile contract. @@ -156,7 +156,6 @@ cat > "$DEPLOY_RECORD" < on-chain profile contract state" echo " admin: $(stellar contract invoke --id "$PROFILE_ID" --source-account "$SOURCE" --network "$NETWORK" -- get_admin)" echo " events_contract: $(stellar contract invoke --id "$PROFILE_ID" --source-account "$SOURCE" --network "$NETWORK" -- get_events_contract)" -echo " default_credits: $(stellar contract invoke --id "$PROFILE_ID" --source-account "$SOURCE" --network "$NETWORK" -- get_default_bootstrap_credits)" echo " is_paused: $(stellar contract invoke --id "$PROFILE_ID" --source-account "$SOURCE" --network "$NETWORK" -- is_paused)"