A decentralized prediction market running directly on Bitcoin via the Charms protocol. Create/resolve/cancel markets, mint/trade/burn YES/NO outcome tokens, claim fees, and redeem winning positions - all on Bitcoin's base layer without requiring sidechains or trusted intermediaries.
Echo Markets enables users to create prediction markets where participants can bet on binary outcomes (Yes/No) by trading fungible tokens. The protocol uses a dual-token system:
- YES tokens: Represent a bet that the outcome will be "Yes"
- NO tokens: Represent a bet that the outcome will be "No"
Users mint complete sets (1 YES + 1 NO) by depositing collateral. After market resolution, holders of winning tokens can redeem them for collateral, while losing tokens become worthless.
- ✅ Native Bitcoin Integration: Runs directly on Bitcoin using Charms protocol
- ✅ Decentralized: No trusted intermediaries
- ✅ Dual-Token System: YES/NO tokens for binary outcomes
- ✅ Fee Collection: Configurable trading fees (basis points)
- ✅ Time-Limited Markets: Trading and resolution deadlines
- ✅ Market Cancellation: Creator can cancel before resolution
- ✅ P2P Trading: Direct token swaps between users
- ✅ Supply Limits: Min/max supply controls to prevent dust and bound market size
- ✅ Comprehensive Testing: Unit tests and integration tests
- ✅ WASM Compatible: Pure Rust implementation compatible with zkVM
State Machine:
[Create]
|
v
[Active] <---> [TradingClosed] (after trading_deadline)
| |
| |
+---> [Resolved] <--+
| |
| |
v v
[Cancelled] [Redeem]
-
Active: Market is open for trading
- Users can mint/burn tokens
- Users can transfer tokens
- Market cannot be resolved yet
-
TradingClosed: Trading deadline has passed
- No new minting/burning allowed
- Token transfers still allowed
- Market can be resolved
-
Resolved: Market outcome has been determined
- No trading or transfers allowed
- Only redemption of winning tokens
- Fees can be claimed by creator
-
Cancelled: Market was cancelled by creator
- Only possible before resolution
- All tokens can be redeemed for refund
- Minting: User deposits
collateral, paysfee = collateral * fee_bps / 10000, receivesshares = collateral - feein YES + NO tokens - Burning: User burns equal YES + NO tokens, receives collateral back (minus fees already paid)
- Trading: Users can transfer YES/NO tokens to each other (P2P trading)
- Redemption: After resolution, winning token holders redeem 1:1 for collateral
trading_deadline: Unix timestamp when trading stopsresolution_deadline: Unix timestamp when resolution becomes availablefee_bps: Trading fee in basis points (100 = 1%)min_bet: Minimum collateral required to mint tokensmax_supply: Maximum total supply of YES/NO tokens
- Rust (latest stable version)
- Charms CLI
- WASM target for Rust
- Install Rust and WASM target:
rustup target add wasm32-wasip1- Clone the repository:
git clone https://github.com/EchoMarkets/echo-markets.git
cd echo-markets- Build the project:
cargo build --release --target wasm32-wasip1The resulting WASM binary will be at ./target/wasm32-wasip1/release/echo-markets.wasm.
Alternatively, use the Charms CLI:
app_bin=$(charms app build)- Get the verification key:
export app_vk=$(charms app vk)# Set environment variables
export in_utxo_0="<your-funding-utxo>"
export market_id=$(echo -n "${in_utxo_0}" | sha256sum | cut -d' ' -f1)
export question_hash=$(echo -n "Will BTC reach 100k by Dec 2025?" | sha256sum | cut -d' ' -f1)
export trading_deadline=1735603200
export resolution_deadline=1735689600
export fee_bps=100
export min_bet=10000
export max_supply=1000000000000
export creator_pubkey="02..." # 33-byte compressed pubkey hex
export addr_0="tb1p..." # Taproot address
# Validate the spell
cat ./spells/create-market.yaml | envsubst | charms spell check --app-bins=${app_bin}# Set up token IDs
export yes_token_id=$(echo -n "${market_id}YES" | sha256sum | cut -d' ' -f1)
export no_token_id=$(echo -n "${market_id}NO" | sha256sum | cut -d' ' -f1)
# Set market state variables
export market_utxo_id="<market-nft-utxo>"
export user_btc_utxo="<user-collateral-utxo>"
export old_yes_supply=0
export old_no_supply=0
export new_yes_supply=990000
export new_no_supply=990000
export mint_amount=1000000
export current_timestamp=$(date +%s)
export addr_market="tb1p..."
export addr_user="tb1p..."
# Validate the spell
cat ./spells/mint-shares.yaml | envsubst | charms spell check --app-bins=${app_bin}# Set market state variables
export market_utxo_id="<market-nft-utxo>"
export user_yes_utxo="<user-yes-tokens-utxo>"
export user_no_utxo="<user-no-tokens-utxo>"
export old_yes_supply=990000
export old_no_supply=990000
export burn_amount=100000 # Amount of sets to burn (must be equal YES + NO)
export new_yes_supply=$((old_yes_supply - burn_amount))
export new_no_supply=$((old_no_supply - burn_amount))
export current_timestamp=$(date +%s)
export addr_market="tb1p..."
export addr_user="tb1p..."
# Validate the spell
cat ./spells/burn-shares.yaml | envsubst | charms spell check --app-bins=${app_bin}export alice_yes_utxo="<alice-yes-tokens-utxo>"
export bob_no_utxo="<bob-no-tokens-utxo>"
export trade_amount=50000
export addr_alice="tb1p..."
export addr_bob="tb1p..."
cat ./spells/trade.yaml | envsubst | charms spell check --app-bins=${app_bin}export market_utxo_id="<market-nft-utxo>"
export outcome="Yes" # or "No" or "Invalid"
export resolver_pubkey="<resolver-public-key-hex>"
export resolution_signature="<signature-hex>"
export resolution_timestamp=$(date +%s)
export yes_supply=990000
export no_supply=990000
export accumulated_fees=10000
export addr_market="tb1p..."
cat ./spells/resolve-market.yaml | envsubst | charms spell check --app-bins=${app_bin}export market_utxo_id="<market-nft-utxo>"
export user_tokens_utxo="<user-tokens-utxo>"
export yes_tokens_burned=990000
export no_tokens_burned=0 # or equal amount if Invalid
export outcome="Yes" # must match market resolution
export addr_market="tb1p..."
export addr_user="tb1p..."
cat ./spells/redeem.yaml | envsubst | charms spell check --app-bins=${app_bin}export market_utxo_id="<market-nft-utxo>"
export creator_signature="<creator-signature-hex>"
export cancel_timestamp=$(date +%s)
export yes_supply=990000
export no_supply=990000
export addr_market="tb1p..."
cat ./spells/cancel-market.yaml | envsubst | charms spell check --app-bins=${app_bin}export market_utxo_id="<market-nft-utxo>"
export accumulated_fees=10000
export yes_supply=990000
export no_supply=990000
export addr_market="tb1p..."
export addr_creator="tb1p..."
cat ./spells/claim-fees.yaml | envsubst | charms spell check --app-bins=${app_bin}cargo testcargo test --test integration_testsThe project includes comprehensive spell testing scripts:
# Dry run (validates YAML structure)
./test-spells.sh
# Real test (requires testnet UTXOs)
./test-spells.sh --real
# Simple validation (no CLI required)
./validate-spells.shGenerate HTML documentation from the Rust code:
# Generate documentation
cargo doc
# Generate and open in browser
cargo doc --open
# Generate docs without dependencies (faster)
cargo doc --no-depsThe documentation will be available at target/doc/echo_markets/index.html and includes all doc comments from the source code.
echo-markets/
├── src/
│ ├── lib.rs # Core contract implementation
│ ├── tests.rs # Unit tests
│ ├── integration_tests.rs # Integration tests
│ └── main.rs # Entry point (for local testing)
├── spells/ # Charms spell definitions
│ ├── create-market.yaml
│ ├── mint-shares.yaml
│ ├── burn-shares.yaml
│ ├── trade.yaml
│ ├── resolve-market.yaml
│ ├── redeem.yaml
│ ├── cancel-market.yaml
│ └── claim-fees.yaml
├── test-spells.sh # Spell testing script
├── validate-spells.sh # Simple spell validation
├── Cargo.toml # Rust dependencies
└── README.md # This file
- Primary Enforcement: Scrolls enforces trading deadlines at the transaction layer
- Contract Validation: Timestamp passed in operation data is validated by the contract
- Defense-in-Depth: Both layers provide security
- Resolution Signatures: Uses Schnorr signatures (k256) for WASM compatibility
- Creator Signatures: Market creator can cancel markets with valid signature
- Oracle Proofs: Cross-chain oracle verification (simplified trusted oracle for Hackathon MVP)
- Min Bet: Prevents dust attacks and ensures meaningful trades
- Max Supply: Bounds market size and prevents overflow
- Checked Arithmetic: All supply operations use checked arithmetic to prevent overflow
- charms-sdk: Charms protocol SDK for Bitcoin apps
- k256: Pure Rust secp256k1 implementation (WASM compatible)
- sha2: SHA256 hashing
- bincode: Binary serialization
- serde: Serialization framework
The project is configured for optimal WASM compilation:
opt-level = 3: Maximum optimizationlto = "fat": Full link-time optimizationpanic = "abort": Smaller binary sizeoverflow-checks = true: Safety checks enabled
All dependencies are pure Rust implementations compatible with wasm32-wasip1 target. The k256 crate is used instead of secp256k1 to avoid C dependencies that don't compile for WASM.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Built with Charms protocol for the The BOS Hackathon - Building Bitcoin Smart Contracts with the BitcoinOS Stack