Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Continuous Integration

on:
push:
branches: ["main"]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: aiken-lang/setup-aiken@v1
with:
version: v1.1.21
- run: aiken fmt --check
- run: aiken check -D
- run: aiken build
6 changes: 6 additions & 0 deletions lazer/cardano/5minBets/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Aiken compilation artifacts
artifacts/
# Aiken's project working directory
build/
# Aiken's default documentation export
docs/
159 changes: 159 additions & 0 deletions lazer/cardano/5minBets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# 5minBets

`5minBets` is a Cardano-native price prediction market written in [Aiken](https://aiken-lang.org). Users bet `UP` or `DOWN` on whether the **gold price in US dollars** (`XAU/USD`) will rise or fall over a fixed 5-minute window, using [Pyth Lazer](https://pyth.network) price data (feed ID `6` — verify before deploying) to determine the outcome.

## Status

The core betting protocol is implemented. `PlaceBet`, `Settle`, `Claim`, and `Refund` actions are fully validated on-chain. See **Design assumptions** for what a production deployment would need to harden further.

## Concept

Each market round follows a simple lifecycle:

1. A relayer opens a round and records the current `XAU/USD` price from Pyth.
2. Users place bets on whether the price will go `UP` or `DOWN` during the next 5 minutes.
3. After the 5-minute window closes, the relayer submits the closing price from Pyth.
4. The contract determines the winning side and enables winners to claim their proportional share of the pool.
5. A 2% protocol fee is deducted before payouts.

### Round lifecycle

```mermaid
sequenceDiagram
actor Relayer
actor User A
actor User B
participant Contract
participant Pyth

Relayer->>Pyth: Fetch XAU/USD opening price
Relayer->>Contract: Create round UTxO (open_price, deadline)

User A->>Contract: PlaceBet(UP, 100 ADA)
User B->>Contract: PlaceBet(DOWN, 50 ADA)

Note over Contract: 5-minute window closes

Relayer->>Pyth: Fetch XAU/USD closing price
Relayer->>Contract: Settle(close_price)
Note over Contract: status → Settled(UP)

User A->>Contract: Claim
Contract-->>User A: ~147 ADA (gross 150 − 3 ADA fee)

Relayer->>Contract: CollectFee (future)
```

### Round states

```mermaid
stateDiagram-v2
[*] --> Open : Relayer creates round UTxO

Open --> Open : PlaceBet (before deadline)
Open --> Settled : Settle — close ≠ open, both sides have bets
Open --> Draw : Settle — close == open
Open --> Void : Settle — one side has no bets

Settled --> Settled : Claim (winner collects, added to claimed list)
Draw --> Draw : Refund (bettor recovers stake)
Void --> Void : Refund (bettor recovers stake)
```

## Design assumptions

The idea is straightforward, but a production-ready version should make these rules explicit:

- Oracle prices must be verifiable on-chain or otherwise constrained by trusted update logic.
- The relayer must not be able to selectively skip, delay, or manipulate round settlement.
- The market should clearly define what happens in edge cases such as equal open/close prices, no bets on one side, or failed settlement.
- Asset support should be precise. If multiple stablecoins are accepted, the contract must define how they are normalized or separated.

## Current repository state

This repo contains an initial prototype of the betting protocol alongside the original placeholder validator:

```text
.github/
workflows/
continuous-integration.yml
validators/
hello_world.ak # original placeholder, kept for reference
round_validator.ak # betting logic: PlaceBet / Settle / Claim / Refund
round_validator_tests.ak # validator tests (22 tests)
lib/
types.ak # RoundDatum, RoundStatus, BetSide, Bet, Action
utils.ak # pool math, winner determination, payout calculation + tests
env/ # empty
aiken.toml
aiken.lock # pins aiken-lang/stdlib v3.0.0 for reproducible builds
plutus.json
.gitignore
README.md
```

The prototype implements the core round mechanics. See **Design assumptions** for what a production version would need to harden.

## Architecture

```text
validators/
round_validator.ak # Core betting logic: PlaceBet, Settle, Claim, Refund
round_validator_tests.ak # Validator tests (imports validate from round_validator)
lib/
types.ak # RoundDatum, RoundStatus, BetSide, Bet, Action
utils.ak # Pool math, winner determination, payout calculation
```

### Validator actions

| Action | Who | When | Effect |
|--------|-----|------|--------|
| `PlaceBet` | Any user | Before deadline | Appends bet to datum, locks ADA |
| `Settle` | Relayer | After deadline | Records close price, sets status |
| `Claim` | Winner | After settlement | Pays net payout, marks as claimed |
| `Refund` | Any bettor | Draw or Void | Returns stake, marks as claimed |

A round UTxO is **created** (not validated) when the relayer sends ADA to the script address with an initial `RoundDatum`. No minting policy is required for the prototype.

## Building

Build the current Aiken project with:

```sh
aiken build
```

This produces compiled Plutus artifacts such as `plutus.json`.

## Testing

Run the test suite with:

```sh
aiken check
```

Run only tests matching a string:

```sh
aiken check -m hello
```

## Documentation

Generate HTML documentation with:

```sh
aiken docs
```

## Continuous integration

A GitHub Actions workflow at `.github/workflows/continuous-integration.yml` runs on every push and pull request. It checks formatting (`aiken fmt --check`), runs tests (`aiken check -D`), and builds the project (`aiken build`).

## Resources

- [Aiken user manual](https://aiken-lang.org)
- [Pyth Lazer documentation](https://docs.pyth.network/lazer)
- [Cardano developer docs](https://developers.cardano.org)
15 changes: 15 additions & 0 deletions lazer/cardano/5minBets/aiken.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# This file was generated by Aiken
# You typically do not need to edit this file

[[requirements]]
name = "aiken-lang/stdlib"
version = "v3.0.0"
source = "github"

[[packages]]
name = "aiken-lang/stdlib"
version = "v3.0.0"
requirements = []
source = "github"

[etags]
18 changes: 18 additions & 0 deletions lazer/cardano/5minBets/aiken.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name = "alex/aiken"
version = "0.0.0"
compiler = "v1.1.21"
plutus = "v3"
license = "Apache-2.0"
description = "Aiken contracts for project 'alex/aiken'"

[repository]
user = "alex"
project = "aiken"
platform = "github"

[[dependencies]]
name = "aiken-lang/stdlib"
version = "v3.0.0"
source = "github"

[config]
62 changes: 62 additions & 0 deletions lazer/cardano/5minBets/lib/types.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use aiken/crypto.{VerificationKeyHash}

/// Pyth Lazer feed ID for XAU/USD (gold priced in US dollars).
/// Verify this against https://pyth.network/developers/price-feed-ids before deploying.
pub const xau_usd_feed_id: Int = 6

/// Minimum bet size in lovelace (2 ADA).
/// Prevents spam bets that bloat the on-chain datum.
pub const min_bet_lovelace: Int = 2000000

pub type BetSide {
Up
Down
}

pub type RoundStatus {
Open
Settled { winner: BetSide }
Draw
Void
}

pub type Bet {
bettor: VerificationKeyHash,
side: BetSide,
amount: Int,
}

/// On-chain datum for a betting round.
///
/// A round is opened by the relayer, who also closes it by submitting
/// the Pyth Lazer closing price for the configured feed. Winners claim
/// their proportional share of the pool minus a 2% protocol fee.
pub type RoundDatum {
/// Pyth Lazer feed ID that this round tracks (must equal xau_usd_feed_id).
feed_id: Int,
/// Key hash of the trusted relayer who opens and settles rounds.
relayer: VerificationKeyHash,
/// Pyth XAU/USD price at round open (scaled integer, e.g. price × 10^8).
open_price: Int,
/// Pyth XAU/USD price at round close — None while the round is open.
close_price: Option<Int>,
/// Current lifecycle state of the round.
status: RoundStatus,
/// POSIX time in milliseconds when the betting window closes.
deadline_ms: Int,
/// All bets placed in this round.
bets: List<Bet>,
/// Bettors who have already collected their winnings or refunds.
claimed: List<VerificationKeyHash>,
}

pub type Action {
/// A user places a bet on a price direction before the deadline.
PlaceBet { bettor: VerificationKeyHash, side: BetSide }
/// The relayer submits the Pyth closing price to resolve the round.
Settle { close_price: Int }
/// A winner claims their proportional share of the pool (minus fee).
Claim { bettor: VerificationKeyHash }
/// A bettor recovers their full stake from a drawn or voided round.
Refund { bettor: VerificationKeyHash }
}
Loading