A Soroban smart contract implementing an oracle-based escrow with dispute resolution, plus the JavaScript integrations that talk to it.
OracleEscrow has no public initialize() function. Its setup arguments
(admin, token, platform, timeout_ledgers) are taken by __constructor,
which the network runs inside the same transaction that creates the contract.
There is no window between the WASM upload and setup for a third party to
claim the contract, so nobody can front-run the deployer's admin assignment.
The guided wizard collects and validates those arguments before it submits anything, then deploys in one call:
./scripts/deploy-init-wizard.shInstances deployed from an earlier WASM that were never initialized are still
open to the old front-running path. Do not call initialize() on them; redeploy
with the constructor instead.
The wizard only updates this repo's world, not the downstream ones. A new contract id has to be propagated by hand to every consumer that pins it:
| Repo | Variable | Where |
|---|---|---|
| arbiter-backend | ORACLE_CONTRACT_ID |
backend env / deployment config |
| arbiter-app | VITE_ORACLE_CONTRACT_ID |
app env / build config |
| arbiter-contract | DEFAULT_CONTRACT_ID |
.github/workflows/release.yml (testnet pin, used by the WASM hash verification) |
The wizard prints the new contract id and these variable names at the end of a run. Until all three agree, release verification and the live backend/app will be pointing at different contracts.
Every current integration with the contract goes through the JavaScript
@stellar/stellar-sdk:
backend/src/stellarClient.js— backend service callsapp/src/contractCalls.js— frontend calls- demo-agent's scripts — scripted demo flows
There is no equivalent for a Rust caller today. soroban-sdk's
#[contractimpl] macro already generates an OracleEscrowClient (the same
type test.rs's client(f) helper returns), which is most of what a
standalone crate would need to publish.
Integrators can talk to the contract from more than one language. The
JavaScript reference (backend/src/stellarClient.js, app/src/contractCalls.js)
remains the canonical example; a Python binding is also shipped under
bindings/python/.
A thin, dependency-light wrapper over the contract's XDR spec. It covers all of the contract's entry points:
- writes:
initialize,submit,deposit,withdraw_balance,charge,resolve,stake,unstake,withdraw,withdraw_to,touch,refund,refund_timeout,set_admin,set_timeout_ledgers - reads:
get_question,get_balance,get_admin,get_timeout_ledgers
The contract's Question, Status, and ContractError types are mapped to
native Python shapes (Question dataclass, Status/ContractError enums)
so callers never touch raw XDR.
from oracle_escrow import Client, Status
client = Client(rpc_url, contract_id, network_passphrase)
question_id = client.submit(asker, question, reward)
client.resolve(oracle, question_id, answer)
assert client.get_question(question_id).status == Status.RESOLVEDThe bindings are generated from the contract's embedded XDR spec, the same
source stellar contract bindings reads. Whenever src/lib.rs's public
interface changes (a new entry point, a new ContractError variant, a changed
Question field), regenerate them with:
stellar contract build
stellar contract bindings typescript \
--wasm target/wasm32-unknown-unknown/release/arbiter_contract.wasm \
--output-dir bindings/typescript
python bindings/python/generate.py \
--wasm target/wasm32-unknown-unknown/release/arbiter_contract.wasm \
--output bindings/python/oracle_escrow/_spec.pyThe Python generator reads the same spec entries the CLI emits, so the two stay in lockstep. Run it as part of the release checklist whenever the public interface changes.
A thin crate wrapping the generated OracleEscrowClient with a stable public
API and its own versioning, independent of the contract crate's own release
cycle. Useful for a future Rust-based backend or CLI tool that wants typed
calls instead of hand-built XDR the way the JS side currently must.
The crate's public types are independent of the contract's own type
versions. Rather than re-exporting the contract's Question, Status,
ContractError, and DataKey directly (which would couple the client
crate's version to the contract's and force a breaking client-crate release
on every contract redeploy — already done twice), the crate wraps them in its
own stable types. This survives a contract redeploy without a breaking
client-crate release each time.
A minimal example equivalent to test.rs's setup()/client() pattern,
demonstrating submit/resolve/refund against a running contract:
use oracle_escrow_client::{Client, Status};
// Equivalent to test.rs's setup(): deploy the contract and build a client.
let client = Client::new(&env, &contract_id);
// submit
let question_id = client.submit(&asker, &question, &reward);
// resolve
client.resolve(&oracle, &question_id, &answer);
assert_eq!(client.status(&question_id), Status::Resolved);
// refund
client.refund(&asker, &question_id);
assert_eq!(client.status(&question_id), Status::Refunded);Building a Rust backend to actually consume this crate — that's a separate, much larger effort.
- docs/economics/incentive-alignment.md —
agent-based simulation of fee/slash/quorum tuning (
sim/incentive/), with settlement math proven identical tolib.rsbysrc/test_incentive_math.rs(#117) - docs/runbooks/refund-timeout-postmortem.md —
what to do when
refund_timeout()activity spikes (#118) - docs/LOAD_TESTING.md — contract, network and backend
load harness (
src/test_load.rs,tools/load/) (#119) - docs/SHADOW_TESTING.md — mirror production traffic
onto a candidate deployment and diff state (
tools/shadow/) (#120) - docs/RPC_CHAOS_TESTING.md — inject RPC faults under
the real question lifecycle and prove nothing is left stuck Pending
(
tools/chaos/,src/test_rpc_chaos.rs) (#121)
This is a design note only. No contract code changes land until the
bridge-to-contract interface (what actually calls submit()/charge(), and
from what address) is settled. src/lib.rs and the tests are intentionally
untouched.
The release profile in Cargo.toml is tuned for size (opt-level = "z",
lto = true, codegen-units = 1, panic = "abort", strip = true), and
stellar contract build runs wasm-opt on the result. Smaller bytecode
lowers the storage rent every deployment (and re-deployment) pays, so the
size is treated as a budget to defend rather than a one-off observation.
Measure it with the same command CI uses:
stellar contract build
ls -l target/wasm32-unknown-unknown/release/arbiter_contract.wasmBaseline: 15.5KB (optimized). The build must stay at or below this number; a PR that regresses it should be visible in CI output. Note the tradeoff: aggressive optimization can make a failed transaction's trap less readable, so debugging a revert may require a separate unoptimized build.
Use the pinned build instead of a bare stellar contract build whenever
the resulting hash matters (release, deployment, or verifying a deployed
contract):
docker build -t arbiter-contract-build .
docker run --rm -v "$PWD:/src" arbiter-contract-build # prints the WASM sha256
# or, on a host with rustup + stellar-cli 27.1.0:
./scripts/reproducible-build.shrustc (rust-toolchain.toml), crate versions (Cargo.lock, --locked),
stellar-cli, build paths, and timestamps are all pinned. The
reproducible-build workflow builds each commit on the runner and in the
container and fails if the two hashes differ. Details and recorded hashes are
in docs/reproducible-builds.md.
get_health() returns open/opened/resolved/refunded counts, default-token
TVL, the configured admin/token/platform, and the pause flag in one call.
set_paused(true) (admin-only) stops new funds from coming in; settlement and
withdrawals keep working. See docs/circuit-breaker.md.
Storage and error-code compatibility rules for changes to lib.rs are in
docs/versioning.md.
CI proves the contract compiles to a valid, deployable WASM, but that alone
doesn't prove a specific deployed contract id corresponds to a specific
lib.rs commit. The wasm-hash-verify workflow closes that gap: it builds the
WASM from the current commit with the same stellar contract build command CI
already runs, fetches the deployed contract's actual on-chain WASM
(stellar contract fetch), and diffs the two sha256 hashes. On a mismatch the
check fails loudly in CI.
This is only meaningful when the build is reproducible (bit-for-bit identical output from the same source) — otherwise a legitimate deployment can show a false mismatch purely from a different build environment. See the reproducible-build work tracked separately.
The target contract id is parameterized. It defaults to the pinned testnet
deployment (CDEZRLCBSRMWT5YLJ5UH3SKLNM5GVTL5TGBWDBMMBEBCFKIG3ZSS5W36) and can
be overridden via the CONTRACT_ID repository variable or the
workflow_dispatch inputs, so the same action is reusable for a future
mainnet deployment:
gh workflow run wasm-hash-verify.yml \
-f contract_id=<contract-id> \
-f network=mainnetThere is no incident-alerting wiring in this repo, so the action does not assume a notification channel exists — a mismatch is surfaced as a failed CI check.
submit() and deposit() both require payer.require_auth() and a direct
token::Client::transfer from an address the contract can authenticate on
Stellar. There is no path today for a payer whose funds originate on another
chain to reach escrow without first bridging to a Stellar-native balance
themselves.
We pick Circle CCTP (native USDC) with a forwarder contract on Stellar, rather than Axelar GMP.
sm
**Baseline: 15.5KB** (optimized). The build must stay at or below this
number; a PR that regresses it should be visible in CI output. Note the
tradeoff: aggressive optimization can make a failed transaction's trap
less readable, so debugging a revert may require a separate unoptimized
build.
## Handsoff notes
<!-- handsoff-issue-28 -->
- #28: touch() never extends Balance's TTL, and every balance-decreasing function (unstake/withdraw/withdraw_balance) skips extend_ttl entirely
<!-- handsoff-issue-76 -->
- #76: Compliance blocklist registry
<!-- handsoff-issue-77 -->
- #77: Travel-rule metadata field
<!-- handsoff-issue-78 -->
- #78: Multi-treasury fee routing
<!-- handsoff-issue-79 -->
- #79: On-chain aggregate stats view
<!-- handsoff-issue-21 -->
- #21: touch() rewrites u
<!-- handsoff-issue-44 -->
- #44: Multisig/timelock admin
<!-- handsoff-issue-45 -->
- #45: DAO-controlled parameters
<!-- handsoff-issue-53 -->
- #53: On-chain worker category tags
<!-- handsoff-issue-54 -->
- #54: Category-specific stake minimums
<!-- handsoff-issue-41 -->
- #41: Stake-weighted voting in resolve()
<!-- handsoff-issue-43 -->
- #43: Slashing insurance for workers