Deployment and operations
A practical guide for deploying a Tholos instance and operating it afterward. For what each function does, see CONTRACT.md. For design rationale, see ARCHITECTURE.md.
Before you deploy
This is testnet-only until audited. See SECURITY.md. Don’t point a Tholos instance at real value on mainnet without an independent security review first.
Decide these parameters up front; none of them (except the resolver committee)
can be changed after initialize:
| Parameter | Guidance |
|---|---|
admin | Passed to stellar contract deploy as a constructor argument, not to initialize. Pinned atomically with contract creation, so nothing at deploy or initialize time can override it; the admin can hand the role to a new address later via propose_admin/accept_admin (see “Rotating the admin key” below). |
token | Any SEP-41 token your users already hold. No swap step exists, so picking a token nobody has is a dead deployment. |
bond_amount | Size from the spam/griefing model in BOND_SIZING.md: start with the larger of the assertion-spam and bad-faith-dispute floors (R_case / tolerated spam per challenge window), add any target attacker-loss margin, check finalize reward economics, then keep the result within the affordability cap for the smallest assertion value you want to support. Also capped at MAX_BOND_AMOUNT, a contract-enforced ceiling well above any realistic bond size. It exists so the bond can never overflow finalize’s reward-multiply arithmetic (the binding constraint) or the token balance held across a dispute. |
challenge_window_secs | Long enough that people who’d actually catch a bad assertion have a realistic chance to see it and act. Short windows finalize faster but catch less. See V1_MAINNET_PARAMETERS.md for sizing guidance by monitoring tier. |
resolvers | Odd-length, non-zero, distinct, and at most 21 addresses. initialize rejects duplicates with DuplicateResolvers. Pick people who’ll actually be reachable to vote within a reasonable time of a dispute; a slow resolver committee stalls every disputed assertion until it acts. See V1_MAINNET_PARAMETERS.md for size and composition trade-offs, and RESOLVER_GOVERNANCE.md for the onboarding process behind picking these addresses in the first place. |
finalize_reward_bps | Basis points (0–1000) of the bond paid to whoever calls finalize. caller must authorize the call unconditionally, even at 0. 0 means no reward: the full bond returns to the asserter. A non-zero value creates an economic incentive for prompt finalization at the cost of a small bond haircut the asserter accepts when posting. 100 bps (1 %) is a reasonable starting point; 1000 bps (10 %) is the maximum enforced by the contract. |
Canonical testnet deployment
Before deploying your own instance, check whether this one already fits: it’s meant to be the shared, long-lived Tholos deployment on testnet, so that trust in the resolver committee’s track record accumulates in one place instead of fragmenting across many one-off deployments. Deploy your own only if you genuinely need different parameters (a different bond size or token, for example); see INTEGRATION.md.
| Field | Value |
|---|---|
| Network | Stellar testnet |
| Contract id | CAOSNC2SKQPGT7WHXKQJQ2RL2J7RECXE5QKZFYIMEHYA3DZTOZG76YYI |
token | Native XLM SAC (CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC) |
bond_amount | 50000000 (5 XLM), per BOND_SIZING.md’s public-testnet/low-value profile |
challenge_window_secs | 21600 (6 hours) |
finalize_reward_bps | 100 (1%) |
resolvers | resolver1/resolver2/resolver3 test identities from this repo’s own testnet workflow, a stopgap: they have no real-world accountability behind them yet. Revisit before treating this instance’s dispute history as trustworthy long-term. |
Deploying
# Build the optimized wasm
cd contracts/tholos && stellar contract build
# Deploy, pinning admin as a constructor argument
CONTRACT=$(stellar contract deploy --wasm target/wasm32v1-none/release/tholos.wasm \
--source deployer --network testnet -- --admin "$ADMIN_ADDRESS")
# Initialize the rest of the deployment-wide policy; requires that same
# admin's signature
stellar contract invoke --id "$CONTRACT" --source deployer --network testnet -- initialize \
--token "$TOKEN_CONTRACT_ID" \
--bond_amount 1000000 \
--challenge_window_secs 3600 \
--resolvers "[\"$R1\",\"$R2\",\"$R3\"]" \
--finalize_reward_bps 0
scripts/testnet-smoke.sh automates this full sequence plus assert/dispute/resolve
against real testnet infrastructure; run it to sanity-check a fresh deploy before
handing the contract id to anyone.
Admin runbook
Rotating the admin key
Rotation is two steps, so a typo’d or unreachable address can never permanently lock out admin control: the current admin proposes a target, and that target must separately accept before authority actually moves.
# Current admin opens the proposal.
stellar contract invoke --id "$CONTRACT" --source admin --network testnet -- propose_admin \
--new_admin "$NEW_ADMIN"
# The proposed address accepts, completing the rotation.
stellar contract invoke --id "$CONTRACT" --source new_admin --network testnet -- accept_admin
accept_admin emits AdminUpdated with both addresses. Afterward, use the
new signer for set_paused, update_resolvers, and future admin rotations.
Note this only ever protects a planned rotation: propose_admin itself
still requires the current admin’s signature, so if the current admin key
is genuinely lost or destroyed (not just being proactively rotated), there is
no on-chain recovery path — see MAINNET_RUNBOOK.md for
what that means for admin key custody on a real deployment.
Pausing during an incident
If something looks wrong (a bug is found, a resolver key looks compromised, vote behavior looks off), pause first and investigate second:
stellar contract invoke --id "$CONTRACT" --source admin --network testnet -- set_paused --paused true
This stops assert_outcome, dispute, resolve, and finalize immediately. A
Pending assertion whose challenge window elapses while paused simply waits, it
becomes finalizable once you unpause, rather than finalizing uncontested during
the incident. Minimize pause duration, since this delays legitimate uncontested
claims for as long as the pause lasts, and unpause with --paused false as soon
as incident handling permits. Do not use pause as a safe migration or retirement
switch.
Rotating the resolver committee
This section covers the on-chain mechanics only: who is authorized to call what, and what each call does. For the off-chain side, who actually becomes a resolver, how their key is generated and secured, what they commit to, and which of the two paths below to use for a given real-world reason (resignation, inactivity, a compromised key, a conflict of interest), see RESOLVER_GOVERNANCE.md.
There are two paths. update_resolvers is the admin emergency override; it works
whether paused or not, so a compromised committee can be replaced without waiting to
unpause:
stellar contract invoke --id "$CONTRACT" --source admin --network testnet -- update_resolvers \
--new_resolvers "[\"$NEW_R1\",\"$NEW_R2\",\"$NEW_R3\"]"
Day to day, the committee rotates itself by a strict majority vote, with no admin key involved. A resolver proposes a single-slot swap, and the rest vote:
# R1 (a current resolver) proposes replacing themselves with R4.
stellar contract invoke --id "$CONTRACT" --source resolver1 --network testnet -- \
propose_rotation --resolver "$R1" --old_resolver "$R1" --new_resolver "$R4"
# Two more resolvers vote yes; with a 3-member committee that's the majority,
# so the rotation executes as soon as the second yes lands.
stellar contract invoke --id "$CONTRACT" --source resolver2 --network testnet -- \
vote_rotation --resolver "$R2" --approve true
stellar contract invoke --id "$CONTRACT" --source resolver3 --network testnet -- \
vote_rotation --resolver "$R3" --approve true
Either path writes the same committee; both emit ResolversUpdated. A rotation has
no effect on disputes already open, because each dispute snapshots the committee at
dispute time. See CONTRACT.md and
docs/src/ROTATION_DESIGN.md for the full detail.
Checking state
Read-only, no auth required:
stellar contract invoke --id "$CONTRACT" --source admin --network testnet -- get_assertion_state --id 0
Mainnet readiness checklist
Not a green light to deploy to mainnet on its own: a checklist of what’s true today, so you can judge what’s still missing for your use case. See MAINNET_RUNBOOK.md for the operational layer this checklist doesn’t cover on its own: admin key custody, a concrete go/no-go launch sequence, and incident escalation.
- Core propose/dispute/resolve flow implemented and unit tested
- Reentrancy hardened, with a regression test proving it
- Admin pause and resolver rotation available for incident response
- Exercised end-to-end against real Stellar testnet infrastructure
- Independent security audit
- Real-world dispute volume tested (all testing so far is synthetic)
- Bond sizing validated against modeled spam/griefing attempts; see BOND_SIZING.md
-
challenge_window_secsand resolver committee size/composition analyzed by monitoring tier and real measured per-operation cost; see V1_MAINNET_PARAMETERS.md. Wall-clock figures there are modeled, not yet confirmed against a live testnet run. See that document’s Part 5. - Fee/reward mechanism for uncontested finalizes (configurable
finalize_reward_bps; see CONTRACT.md)