Skip to content

Commit 6c8061d

Browse files
committed
Merge branch 'main' into feature/frontend-base
2 parents 70dc5af + e8ccd74 commit 6c8061d

6 files changed

Lines changed: 797 additions & 30 deletions

File tree

CLAUDE.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project
6+
7+
**Synth Peso** — a synthetic ADA/USD asset on Cardano, using Pyth Lazer as the price oracle. Users mint synth tokens by depositing ADA (price determined by oracle), and burn synth tokens to reclaim the corresponding ADA from the pool.
8+
9+
## Repository layout
10+
11+
```
12+
on-chain/ Aiken smart contracts (los-magnificos/synth-peso)
13+
reference/ Original hackathon repo examples (entropy, lazer, price_feeds) — not used
14+
```
15+
16+
## On-chain (Aiken)
17+
18+
All contract work happens inside `on-chain/`. Commands must be run from that directory.
19+
20+
```bash
21+
cd on-chain
22+
aiken check # type-check + run all tests
23+
aiken build # compile to Plutus blueprints → plutus.json
24+
aiken check -m foo # run only tests matching pattern "foo"
25+
aiken docs # generate HTML documentation
26+
```
27+
28+
### Dependencies (`aiken.toml`)
29+
- `aiken-lang/stdlib v3.0.0`
30+
- `pyth-network/pyth-lazer-cardano` (pinned commit `f78b676`) — Pyth Lazer on-chain SDK
31+
32+
### Pyth Lazer integration
33+
34+
The key entry point from `pyth-lazer-cardano`:
35+
36+
```aiken
37+
use pyth.{get_updates, PriceUpdate, Feed}
38+
39+
let updates: List<PriceUpdate> = pyth.get_updates(pyth_policy_id, transaction)
40+
```
41+
42+
`get_updates` requires:
43+
- A **Pyth State NFT** reference input (PolicyId passed as parameter, token name `"Pyth State"`)
44+
- Price messages submitted via the **withdraw script redeemer** as `List<ByteArray>` (handled by the oracle relayer, not our contract)
45+
46+
`get_updates` reads the Pyth redeemer internally via:
47+
```aiken
48+
pairs.get_first(tx.redeemers, Withdraw(Script(withdraw_script_hash)))
49+
```
50+
The redeemer form is `List<ByteArray>` — each `ByteArray` is a signed Pyth price message. The withdraw script verifies the Ed25519 signature on each message; by the time our validator runs, the price is already authenticated.
51+
52+
Each `ByteArray` message has the following binary structure (Solana wire format):
53+
```
54+
[4 bytes] magic: b9011a82 (little-endian)
55+
[64 bytes] Ed25519 signature
56+
[32 bytes] public key
57+
[2 bytes] payload length (little-endian u16)
58+
[N bytes] payload
59+
```
60+
61+
The payload itself is structured as:
62+
```
63+
[4 bytes] magic: 75d3c793 (little-endian)
64+
[8 bytes] timestamp_us (little-endian u64)
65+
[1 byte] channel_id
66+
[1 byte] number of feeds
67+
[...] feeds (each: 4-byte feed_id + 1-byte property count + properties)
68+
```
69+
70+
Each `Feed` contains:
71+
- `feed_id: U32` — asset identifier (e.g., ADA/USD has a specific ID)
72+
- `price: Option<Option<Int>>` — raw integer price (`Some(None)` = unavailable, `Some(Some(p))` = valid)
73+
- `exponent: Option<Int>` — scale factor; real price = `price × 10^exponent`
74+
75+
### Protocol specification
76+
77+
**Mint (ADA → synth USD):**
78+
- Off-chain: query Pyth Lazer API for ADA/USD price + signature; build tx with Pyth State as reference input + 0-withdrawal from Pyth verify script carrying the signed price bytes as redeemer
79+
- On-chain inputs: pool UTxO (ADA collateral) + user's ADA
80+
- On-chain outputs: minted synth tokens to user + new pool UTxO (with increased ADA)
81+
82+
**Burn (synth USD → ADA):**
83+
- Off-chain: same oracle query + tx construction
84+
- On-chain inputs: pool UTxO + user's synth tokens
85+
- On-chain outputs: ADA returned to user + new pool UTxO (with decreased ADA)
86+
87+
**Key insight — no UTxO contention:** The Pyth State NFT UTxO is **never spent**. It is always a `reference_input`. The price update is pushed by the user as a withdrawal redeemer (`Withdraw(Script(pyth_withdraw_script))`). The withdraw script verifies the Ed25519 signature. Multiple users can mint/burn in the same block without contention.
88+
89+
### Architecture
90+
91+
Two validators run together in every mint/burn transaction:
92+
93+
1. **`mint` policy** — controls synth token supply; verifies oracle price and that the correct amount of synth tokens is minted/burned relative to ADA deposited/withdrawn
94+
2. **`spend` validator** — guards the **pool UTxO** (ADA collateral); enforces the eUTxO state machine (pool UTxO must be spent and recreated with updated ADA balance)
95+
96+
The **Pyth State NFT** is a **reference input** (not spent) — the oracle relayer submits price messages via a withdraw script redeemer in the same transaction. The pool UTxO is what gets spent and recreated each time.
97+
98+
### Validator structure
99+
100+
`validators/placeholder.ak` is the entry point to replace. Target structure:
101+
- `mint` handler — oracle price lookup, synth amount calculation, allow mint/burn
102+
- `spend` handler — pool UTxO continuity check, ADA balance update
103+
104+
### Plutus version
105+
Plutus v3 — use `ScriptContext` patterns accordingly.

README.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
🚀 Pyth Cardano Hackathon 2026
2+
3+
Team: Los Magníficos!
4+
Agustin Salinas (@AgustinBadi)
5+
Mauricio Navarrete (@lordkhyron)
6+
Rodrigo Oyarzun (@Rodrigoioyz)
7+
Contact: librenotgratis@tuta.io
8+
9+
📋 Contribution Information
10+
11+
| Category | Details |
12+
|---|---|
13+
| Contribution Type | ✅ Hackathon Submission |
14+
| Project Name | Synth Peso |
15+
| Pyth Product | 🟢 Pyth Price Feeds (Pyth Lazer) |
16+
| Blockchain | ₳ Cardano |
17+
18+
---
19+
20+
## 📝 What is Synth Peso?
21+
22+
**Synth Peso** is a synthetic ADA/USD stablecoin protocol built on Cardano. Users lock ADA as collateral and mint synth tokens pegged to the USD value of that ADA, as determined in real-time by the **Pyth Lazer oracle**. Burning synth tokens returns the corresponding ADA from the collateral pool.
23+
24+
The protocol enforces:
25+
- **Overcollateralization** — you can only mint a fraction of your ADA's USD value (controlled by `collateral_ratio`)
26+
- **Health checks on withdrawal** — you cannot burn synth and withdraw ADA if it leaves the position below the liquidation threshold
27+
- **Open liquidation** — anyone can liquidate an undercollateralized position
28+
- **Owner-only burns** — only the position owner (verified via signature) can voluntarily burn and withdraw
29+
30+
---
31+
32+
## ⚙️ How It Works
33+
34+
### Mint (ADA → Synth USD)
35+
36+
1. User sends ADA to the pool UTxO
37+
2. The on-chain validator reads the live ADA/USD price from Pyth Lazer
38+
3. It computes: `synth_to_mint = (ada_deposited × collateral_ratio / 100) × price`
39+
4. The minting policy mints exactly that amount of synth tokens to the user
40+
41+
### Burn (Synth USD → ADA)
42+
43+
1. User specifies how much ADA to withdraw from the pool
44+
2. Validator reads live oracle price
45+
3. Computes synth to burn: `synth_burned = ada_withdrawn × raw_price / 10^abs_exp`
46+
4. Checks remaining position health: `health = (remaining_ada × raw_price / 10^abs_exp) / remaining_debt ≥ liquidation_threshold`
47+
5. Verifies the transaction is signed by the position owner
48+
6. ADA is released from the pool
49+
50+
### Liquidate
51+
52+
1. Any user can trigger liquidation on an undercollateralized position (`health < liquidation_threshold`)
53+
2. The liquidator burns synth tokens and receives the corresponding ADA from the pool
54+
3. No owner signature required — the position's health condition is the only gate
55+
56+
---
57+
58+
## 🔮 How Pyth Lazer is Used
59+
60+
The contract uses [`pyth-network/pyth-lazer-cardano`](https://github.com/pyth-network/pyth-lazer-cardano) (pinned at commit `f78b676`).
61+
62+
### On-chain price reading
63+
64+
```aiken
65+
let updates = pyth.get_updates(pyth_policy_id, tx)
66+
expect [update] = updates
67+
expect Some(feed) = list.find(feeds, fn(f) { u32.as_int(f.feed_id) == ada_usd_feed_id })
68+
expect Some(Some(raw_price)) = feed.price
69+
expect Some(exponent) = feed.exponent
70+
// real_price = raw_price × 10^exponent
71+
```
72+
73+
### How the price reaches the contract
74+
75+
Every mint/burn/liquidate transaction must include:
76+
77+
1. **Pyth State NFT** as a reference input (identified by `pyth_policy_id`) — never spent
78+
2. **A 0-ADA withdrawal** from the Pyth verify script, carrying the signed price message as the redeemer
79+
80+
The Pyth verify script validates the **Ed25519 signature** on each price message before the main validator runs. By the time `get_updates` is called, the price is already cryptographically authenticated.
81+
82+
### Price feed
83+
84+
- **Asset:** ADA/USD
85+
- **Feed ID:** 16
86+
- **Exponent:** -8 (i.e. `raw_price = 70_000_000``$0.70 per ADA`)
87+
- **No contention:** The Pyth State NFT is a reference input — multiple users can mint/burn in the same block without UTxO conflicts
88+
89+
---
90+
91+
## 📐 Protocol Parameters
92+
93+
These values are set at deployment time and enforced entirely on-chain:
94+
95+
| Parameter | Value | Description |
96+
|---|---|---|
97+
| `collateral_ratio` | **150%** | You can mint at most 66% of your ADA's USD value in synth tokens |
98+
| `liquidation_threshold` | **120%** | Positions below this health ratio can be liquidated by anyone |
99+
| `ada_usd_feed_id` | **16** | Pyth Lazer feed ID for ADA/USD |
100+
101+
**Example:** Depositing 100 ADA at $0.70/ADA ($70 collateral value) → you can mint up to **$46.67 of synth-USD**. If ADA drops to $0.56, your health ratio hits 120% and the position becomes liquidatable.
102+
103+
---
104+
105+
## 👤 User Flow
106+
107+
### Minting synth-USD
108+
1. User sends ADA to the protocol pool UTxO
109+
2. On-chain validator fetches live ADA/USD price from Pyth Lazer
110+
3. Calculates max synth: `synth = ada × price × (100 / collateral_ratio)`
111+
4. Minting policy issues exactly that amount of synth tokens to the user's wallet
112+
113+
### Burning synth-USD (withdraw ADA)
114+
1. User decides how much ADA to withdraw
115+
2. Validator fetches live price from Pyth Lazer
116+
3. Calculates synth to burn: `synth_burned = ada_withdrawn × price`
117+
4. Verifies remaining position stays above 120% health
118+
5. User signs the transaction — synth is burned, ADA returned
119+
120+
### Liquidation (undercollateralized position)
121+
1. ADA price drops → a position's health falls below 120%
122+
2. Any user can call `Liquidate`
123+
3. Liquidator burns synth tokens, receives the equivalent ADA from the pool
124+
4. No owner signature required — the health condition is the only gate
125+
126+
---
127+
128+
## 🛡️ Quality Assurance & Reliability
129+
130+
### Edge cases handled on-chain
131+
- **Zero deposit/withdrawal blocked:** `ada_deposited >= 1` and `ada_withdrawn >= 1` enforced explicitly
132+
- **Zero debt guard:** `health_ratio` fails immediately if `debt_amount == 0` — prevents division by zero
133+
- **Double Option price unwrap:** Pyth feed returns `Option<Option<Int>>` — the validator explicitly handles `None` (field missing) and `Some(None)` (price unavailable), failing both cases
134+
- **Single update enforced:** `expect [update] = updates` — rejects transactions with zero or multiple price messages
135+
136+
### Oracle failure handling
137+
- If the Pyth withdraw script is not included in the transaction, `pyth.get_updates` returns an empty list and the validator fails — **no stale or missing price is ever accepted**
138+
- The Ed25519 signature on each price message is verified by the Pyth verify script before our validator runs — invalid or replayed messages are rejected at the protocol level
139+
140+
### Price anomaly protection
141+
- Price is read fresh from the oracle in every transaction — there is no cached or stored price in the datum
142+
- The `collateral_ratio` and `liquidation_threshold` parameters provide a safety buffer against sudden price moves
143+
144+
---
145+
146+
## 💼 Business Development & Viability
147+
148+
### Target users
149+
- ADA holders who want USD-denominated liquidity without selling their ADA
150+
- DeFi users on Cardano seeking synthetic exposure to USD
151+
- Protocols that need a decentralized, oracle-backed stablecoin primitive
152+
153+
### Market need
154+
Cardano has existing decentralized stablecoins (DJED by COTI, iUSD by Indigo Protocol). Synth Peso **expands the offering** with a lightweight, single-collateral design that uses Pyth Lazer — a battle-tested, high-frequency oracle — rather than a custom price mechanism. This brings institutional-grade price feeds to Cardano CDP protocols.
155+
156+
### Competitive positioning
157+
158+
| Protocol | Oracle | Collateral | Chain |
159+
|---|---|---|---|
160+
| DJED (COTI) | Custom | ADA | Cardano |
161+
| iUSD (Indigo) | Chainlink | ADA | Cardano |
162+
| MakerDAO (DAI) | Chainlink | ETH/multi | Ethereum |
163+
| **Synth Peso** | **Pyth Lazer** | **ADA** | **Cardano** |
164+
165+
Pyth Lazer offers sub-second price updates and is already used across 50+ chains — giving Synth Peso a credibility advantage at launch.
166+
167+
### Revenue model
168+
Protocol fees collected on mint and liquidation events (configurable via protocol parameters). Fee revenue funds ongoing development and can be directed to a DAO treasury as the protocol matures.
169+
170+
### Scalability
171+
- **No UTxO contention:** The Pyth State NFT is a reference input — any number of users can mint or burn in the same block without competing for the same UTxO
172+
- **Permissionless liquidation:** Anyone can liquidate, eliminating the need for a centralized keeper network
173+
- **Pyth partnership potential:** As Pyth expands its Cardano presence, Synth Peso is positioned to add new synthetic assets (BTC/USD, ETH/USD) by simply deploying new validator instances with different feed IDs
174+
175+
---
176+
177+
## 🛠️ How to Build
178+
179+
### Prerequisites
180+
181+
- [Aiken](https://aiken-lang.org) v1.1.19
182+
183+
```bash
184+
aikup install v1.1.19
185+
```
186+
187+
### Build
188+
189+
```bash
190+
cd on-chain
191+
aiken build
192+
```
193+
194+
This compiles the contracts and generates `on-chain/plutus.json` (the Plutus blueprint).
195+
196+
### Run tests
197+
198+
```bash
199+
cd on-chain
200+
aiken check
201+
```
202+
203+
All 25 unit tests in `lib/utils.ak` cover:
204+
- `health_ratio` — collateral/debt ratio calculation
205+
- `can_adjust` / `is_liquidatable` — position health gates
206+
- `liquidator_payout` / `protocol_payout` — ADA distribution on liquidation
207+
- `compute_expected_synth_amount` — ADA → synth USD conversion (mint and burn directions)
208+
209+
### Project structure
210+
211+
```
212+
on-chain/
213+
validators/
214+
synth-dolar.ak # Main validator: mint policy + spend guard
215+
lib/
216+
utils.ak # Math helpers + 25 unit tests
217+
types/
218+
cdp.ak # CdpDatum type
219+
aiken.toml # Dependencies
220+
```
221+
222+
---
223+
224+
✅ Quality Checklist
225+
226+
- [x] Make it beautiful: Clean hierarchy and formatting.
227+
- [x] Code Standards: Follows existing repository patterns.
228+
- [x] Security: No hardcoded values; uses environment variables.
229+
- [x] Verification: Locally tested and verified.

on-chain/lib/types/cdp.ak

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// Estado del CDP (Collateralized Debt Position) almacenado en el datum del pool UTxO.
2+
///
3+
/// - `ada_locked` : colateral depositado, en lovelaces (1 ADA = 1_000_000)
4+
/// - `minted_amount` : deuda emitida en synth tokens, en micro-USD (6 decimales)
5+
/// - `owner` : hash de la clave del dueño de esta posición
6+
pub type CdpDatum {
7+
ada_locked: Int,
8+
minted_amount: Int,
9+
owner: ByteArray,
10+
}

0 commit comments

Comments
 (0)