Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
d683c51
Update README.md
rodrigoioyz Mar 22, 2026
c341f32
Update contact information and contribution types
rodrigoioyz Mar 22, 2026
8c5eb8e
Update README.md
rodrigoioyz Mar 22, 2026
f47ddce
Create initial hackathon project structure
AgustinBadi Mar 22, 2026
c2bd908
move .git files
AgustinBadi Mar 22, 2026
19ea768
init frontend structure
lordkhyron Mar 22, 2026
fe4c46c
Create validator
AgustinBadi Mar 22, 2026
251ae44
Add get_ada_usd_price function
AgustinBadi Mar 22, 2026
24d17cf
Add empty file on lib
AgustinBadi Mar 22, 2026
f999c63
add helper functions to get price and minted amount
AgustinBadi Mar 22, 2026
ffaeb1d
Add utils, CdpDatum type and Liquidate action
rodrigoioyz Mar 22, 2026
2b5c185
feat(frontend): dashboard UI with chart and NFT card + fix Mesh/Vite …
lordkhyron Mar 22, 2026
25c07f7
Finish mint condition
AgustinBadi Mar 22, 2026
4b49de1
Revert synth-dolar.ak to pre-session state
rodrigoioyz Mar 22, 2026
eabc840
Add unit tests to utils.ak
rodrigoioyz Mar 22, 2026
eb5ced9
Merge branch 'main' of https://github.com/rodrigoioyz/Cardano-Pyth-Ha…
AgustinBadi Mar 22, 2026
adef158
Create README.md
rodrigoioyz Mar 22, 2026
a670e85
Add burn condition
AgustinBadi Mar 22, 2026
4f93f96
Merge branch 'main' of https://github.com/rodrigoioyz/Cardano-Pyth-Ha…
AgustinBadi Mar 22, 2026
9167153
Add compute_expected_synth_amount to utils.ak
rodrigoioyz Mar 22, 2026
4dc72cb
Add burn direction tests for compute_expected_synth_amount
rodrigoioyz Mar 22, 2026
65dc004
Finish first iteration of the contract
AgustinBadi Mar 22, 2026
f5bc902
Create spec and add it on context file
AgustinBadi Mar 22, 2026
c9686ff
Validator fix on collateral calc
AgustinBadi Mar 22, 2026
58eef0f
chore(backend): add gitignore and backend setup
lordkhyron Mar 22, 2026
6dd13d2
Write README with full project documentation
rodrigoioyz Mar 22, 2026
3a78f74
Fix burn formula direction in README
rodrigoioyz Mar 22, 2026
70dc5af
feat(fullstack): integrate frontend dashboard with backend Pyth servi…
lordkhyron Mar 22, 2026
e8ccd74
Add protocol parameters, user flow, QA and business sections to README
rodrigoioyz Mar 22, 2026
6c8061d
Merge branch 'main' into feature/frontend-base
lordkhyron Mar 22, 2026
f29f808
Merge pull request #1 from rodrigoioyz/feature/frontend-base
lordkhyron Mar 22, 2026
9d2cfad
Add test and minor fixes
AgustinBadi Mar 22, 2026
eccf88f
Merge branch 'main' of https://github.com/rodrigoioyz/Cardano-Pyth-Ha…
AgustinBadi Mar 22, 2026
5e84b54
Add frontend and backend run instructions to README
rodrigoioyz Mar 22, 2026
7613ba5
Create tx folder inside frontend && create shared config && build plu…
AgustinBadi Mar 22, 2026
7b09213
Create empty tx files && fix backend format to fetch the price of pyt…
AgustinBadi Mar 22, 2026
e66db86
Merge branch 'main' of https://github.com/rodrigoioyz/Cardano-Pyth-Ha…
AgustinBadi Mar 22, 2026
c373b87
Add backend API documentation to README
rodrigoioyz Mar 22, 2026
b145fe9
add policy_id of pyth to constants
AgustinBadi Mar 22, 2026
ae372a7
Merge branch 'main' of https://github.com/rodrigoioyz/Cardano-Pyth-Ha…
AgustinBadi Mar 22, 2026
d11bb0c
Add Pyth PreProd policy ID to README
rodrigoioyz Mar 22, 2026
4aa1450
Merge branch 'main' of https://github.com/rodrigoioyz/Cardano-Pyth-Ha…
AgustinBadi Mar 22, 2026
e78f6b7
WIP of tx functions
AgustinBadi Mar 22, 2026
45741cc
wip on tx function #2
AgustinBadi Mar 22, 2026
efea417
wip on tx function #3
AgustinBadi Mar 22, 2026
9f4ca71
wip #3
AgustinBadi Mar 22, 2026
0d07e13
wip #4
AgustinBadi Mar 22, 2026
fc533f7
fix contract bug
AgustinBadi Mar 22, 2026
8129930
botones mint burn
rodrigoioyz Mar 22, 2026
c9e7f35
Improve tx functions
AgustinBadi Mar 22, 2026
5991552
Fix mint.ts
AgustinBadi Mar 22, 2026
01bda65
Merge remote-tracking branch 'origin/main'
AgustinBadi Mar 22, 2026
fe35212
bbb
rodrigoioyz Mar 22, 2026
08be850
almostontime
rodrigoioyz Mar 23, 2026
5eb09c3
Update README with images and team details
rodrigoioyz Mar 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"snyk.advanced.autoSelectOrganization": true
}
271 changes: 271 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project

**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.

## Repository layout

```
on-chain/ Aiken smart contracts (los-magnificos/synth-peso)
reference/ Original hackathon repo examples (entropy, lazer, price_feeds) — not used
```

## On-chain (Aiken)

All contract work happens inside `on-chain/`. Commands must be run from that directory.

```bash
cd on-chain
aiken check # type-check + run all tests
aiken build # compile to Plutus blueprints → plutus.json
aiken check -m foo # run only tests matching pattern "foo"
aiken docs # generate HTML documentation
```

### Dependencies (`aiken.toml`)
- `aiken-lang/stdlib v3.0.0`
- `pyth-network/pyth-lazer-cardano` (pinned commit `f78b676`) — Pyth Lazer on-chain SDK

### Pyth Lazer integration

The key entry point from `pyth-lazer-cardano`:

```aiken
use pyth.{get_updates, PriceUpdate, Feed}

let updates: List<PriceUpdate> = pyth.get_updates(pyth_policy_id, transaction)
```

`get_updates` requires:
- A **Pyth State NFT** reference input (PolicyId passed as parameter, token name `"Pyth State"`)
- Price messages submitted via the **withdraw script redeemer** as `List<ByteArray>` (handled by the oracle relayer, not our contract)

`get_updates` reads the Pyth redeemer internally via:
```aiken
pairs.get_first(tx.redeemers, Withdraw(Script(withdraw_script_hash)))
```
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.

Each `ByteArray` message has the following binary structure (Solana wire format):
```
[4 bytes] magic: b9011a82 (little-endian)
[64 bytes] Ed25519 signature
[32 bytes] public key
[2 bytes] payload length (little-endian u16)
[N bytes] payload
```

The payload itself is structured as:
```
[4 bytes] magic: 75d3c793 (little-endian)
[8 bytes] timestamp_us (little-endian u64)
[1 byte] channel_id
[1 byte] number of feeds
[...] feeds (each: 4-byte feed_id + 1-byte property count + properties)
```

Each `Feed` contains:
- `feed_id: U32` — asset identifier (e.g., ADA/USD has a specific ID)
- `price: Option<Option<Int>>` — raw integer price (`Some(None)` = unavailable, `Some(Some(p))` = valid)
- `exponent: Option<Int>` — scale factor; real price = `price × 10^exponent`

### Protocol specification

**Mint (ADA → synth USD):**
- 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
- On-chain inputs: pool UTxO (ADA collateral) + user's ADA
- On-chain outputs: minted synth tokens to user + new pool UTxO (with increased ADA)

**Burn (synth USD → ADA):**
- Off-chain: same oracle query + tx construction
- On-chain inputs: pool UTxO + user's synth tokens
- On-chain outputs: ADA returned to user + new pool UTxO (with decreased ADA)

**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.

### Architecture

Two validators run together in every mint/burn transaction:

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
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)

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.

### Validator structure

`validators/synth-dolar.ak` — multi-validator with four compile-time parameters:

```aiken
validator synth_dolar(
pyth_policy_id: PolicyId, -- Pyth State NFT policy (testnet/mainnet differ)
ada_usd_feed_id: Int, -- 16 (ADA/USD on Pyth Lazer)
collateral_ratio: Int, -- e.g. 150 = 150% collateral requirement
liquidation_threshold: Int, -- e.g. 120 = liquidate when health drops below 120%
)
```

#### Types

```aiken
pub type PoolDatum {
owner: ByteArray -- pubkey hash of the position owner
}

pub type Action { Mint | Burn | Liquidate }
```

#### `mint` handler — controls synth token supply

**Mint:**
1. ADA delta (pool output − pool input) must be ≥ 1 lovelace
2. Collateralized ADA = `ada_deposited × collateral_ratio / 100`
3. `minted_amount == compute_expected_synth_amount(collateralized_ada, raw_price, exponent)`

**Burn:**
1. ADA withdrawal must be ≥ 1 lovelace
2. `minted_amount == -compute_expected_synth_amount(ada_withdrawn, raw_price, exponent)`
3. Health after withdrawal ≥ `liquidation_threshold` (position must stay solvent)
4. Transaction must be signed by `PoolDatum.owner`

**Liquidate:**
1. ADA withdrawal must be ≥ 1 lovelace
2. Same burn math as Burn
3. Health after withdrawal < `liquidation_threshold` (position must be unhealthy)
4. No owner signature required — anyone can liquidate

#### `spend` handler — guards pool UTxO

Delegates all validation to the `mint` policy. Only checks that the mint policy is running in the same transaction (`get_minted_amount(mint, policy_id) != 0`). The `policy_id` is derived from the spent UTxO's script address.

#### `else` handler

`fail` — rejects all other script purposes (staking, governance, etc.)

#### Helper functions (below validator)

| Function | Purpose |
|---|---|
| `get_ada_usd_price` | Fetches `(raw_price, exponent)` from Pyth Lazer via `get_updates` |
| `get_ada_delta` | Returns `output_lovelace - input_lovelace` for the pool UTxO |
| `get_pool_ada` | Returns current lovelace balance of the pool input UTxO |
| `get_minted_amount` | Sums all token quantities minted/burned under a policy |

#### Price formula

```
synth_micro = ada_lovelaces × raw_price / 10^8
```
(ADA/USD exponent = −8; synth tokens have 6 decimals like ADA lovelaces)

### Plutus version
Plutus v3 — use `ScriptContext` patterns accordingly.

## Frontend (TypeScript / MeshSDK)

Located in `frontend/src/tx/`. Uses MeshSDK v1.9.0-beta + Blockfrost as provider (preprod).

### Key files

| File | Purpose |
|---|---|
| `contract.ts` | All shared constants: script CBOR, protocol params, Pyth addresses, amount helpers |
| `mint.ts` | `buildMintTx` — deposit ADA, mint synth tokens |
| `burn.ts` | `buildBurnTx` — burn synth tokens, reclaim ADA |
| `liquidate.ts` | `buildLiquidateTx` — permissionless liquidation of undercollateralised positions |

### Important: script CBOR encoding

Aiken's `plutus.json` outputs `compiledCode` in **single-CBOR** encoding. MeshSDK's `applyParamsToScript` requires **double-CBOR** encoding. Always wrap with `applyCborEncoding` before passing to `applyParamsToScript`:

```typescript
const UNPARAMETERISED_SCRIPT_CBOR = applyCborEncoding(RAW_COMPILED_CODE);
```

This is already done in `contract.ts` — do not remove it.

### Script address derivation

```typescript
const scriptCbor = applyParamsToScript(UNPARAMETERISED_SCRIPT_CBOR, [
PARAMS.PYTH_POLICY_ID,
PARAMS.ADA_USD_FEED_ID,
PARAMS.COLLATERAL_RATIO,
PARAMS.LIQUIDATION_THRESHOLD,
]);
const poolAddress = serializePlutusScript({ code: scriptCbor, version: "V3" }, undefined, 0).address;
const scriptHash = resolvePlutusScriptHash(poolAddress); // also the minting policy ID
```

Known derived values (preprod):
- `poolAddress` → `addr_test1wqkhsggq87fndzsll52yp6rm9aw6jw9hhpaenjzxag0xazq3wlxky`
- `scriptHash` → `2d7821003f93368a1ffd1440e87b2f5da938b7b87b99c846ea1e6e88`

### Known preprod constants (`contract.ts`)

```typescript
PARAMS.PYTH_POLICY_ID = "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6"
PARAMS.ADA_USD_FEED_ID = 16
PARAMS.COLLATERAL_RATIO = 150
PARAMS.LIQUIDATION_THRESHOLD = 120

PYTH.STATE_ADDRESS = "addr_test1wrm3tr5zpw9k2nefjtsz66wfzn6flnphr5kd6ak9ufrl3wcqqfyn8"
PYTH.STATE_ASSET_NAME = "50797468205374617465" // hex("Pyth State")
// Asset fingerprint: asset1kjr4k3m0xe5c747n6yv2s9dlfhkmzgceqs82jy (verified via CIP-14)

PYTH.WITHDRAW_SCRIPT_CBOR = "TODO" // Pyth verify script — get from Pyth team
PYTH.WITHDRAW_ADDRESS = "TODO" // reward address of the verify script
```

### Mesh "Data" format for redeemers/datums

In MeshSDK's "Mesh" encoding (used with `"Mesh"` flag on builder calls):
- `ByteArray` → plain hex string
- `List<T>` → JS array `[...]`
- `Constr(N, fields)` → `mConStr0([...])` / `mConStr1([...])` / `mConStr2([...])` from `@meshsdk/core`

Do **not** use `mBytes` or `mList` — they are not exported by `@meshsdk/core`. Use hex strings and arrays directly.

### Redeemer mapping

| Action | Spend redeemer | Mint redeemer |
|---|---|---|
| Mint | `mConStr0([])` | `mConStr0([])` |
| Burn | `mConStr1([])` | `mConStr1([])` |
| Liquidate | `mConStr2([])` | `mConStr2([])` |

Pyth withdrawal redeemer: `[pythHex]` — a JS array containing the hex-encoded Solana wire format price message returned by the backend as `solanaPayload`.

### Pyth price message (`pythHex` / `solanaPayload`)

The backend (`/price` endpoint) returns `solanaPayload` — a hex string of the Solana wire format signed price message:

```
[4 bytes] magic: b9 01 1a 82
[64 bytes] Ed25519 signature
[32 bytes] public key
[2 bytes] payload length (u16 LE)
[4 bytes] payload magic: 75 d3 c7 93
[8 bytes] timestamp_us (u64 LE)
[1 byte] channel_id
[1 byte] feed count
[4 bytes] feed_id = 16 (ADA/USD, u32 LE)
[...] properties: Price (i64 LE), Exponent (i16 LE), ...
```

This is passed as the withdrawal redeemer to the Pyth verify script, which validates the Ed25519 signature before our validator runs.

### Amount helpers

```typescript
// Synth to mint for a given ADA deposit
computeMintAmount(lovelaces, price) = (lovelaces × rawPrice / 1e8) × 100 / collateralRatio

// ADA to return when burning synth
computeBurnReturn(synthMicro, price) = synthMicro × 1e8 / rawPrice
```

Where `rawPrice = Math.round(price * 1e8)` (price is the float ADA/USD value).
Loading