Skip to content

Pythacoin – CDP stablecoin on Cardano using Pyth Lazer#124

Open
nau wants to merge 1 commit intopyth-network:mainfrom
nau:pythacoin-submission
Open

Pythacoin – CDP stablecoin on Cardano using Pyth Lazer#124
nau wants to merge 1 commit intopyth-network:mainfrom
nau:pythacoin-submission

Conversation

@nau
Copy link
Copy Markdown

@nau nau commented Mar 22, 2026

Pyth Examples Contribution

Team Information

  • Team Name: LANTR
  • Submission Name: Pythacoin
  • Team Members: Captain Alex Nemish (@nau)
  • Contact: alex@lantr.io

Type of Contribution

  • New Example Project
  • Bug Fix
  • Documentation Update
  • Enhancement
  • Hackathon Submission

Project Information

Project/Example Name: Pythacoin – CDP-based stablecoin on Cardano using Pyth Lazer

Pyth Product Used:

  • Pyth Price Feeds
  • Pyth Entropy
  • Multiple Products
  • Other

Blockchain/Platform:

  • Ethereum/EVM
  • Solana
  • Aptos
  • Sui
  • Fuel
  • Starknet
  • TON
  • Other: Cardano

Description

What does this contribution do?

Pythacoin is a fully functional CDP (Collateralized Debt Position) stablecoin protocol on Cardano. Users lock ADA as collateral to mint PUSD, a synthetic USD-pegged stablecoin. The protocol enforces a 95% max LTV for minting and a 90% liquidation threshold — when a CDP becomes undercollateralized, anyone can liquidate it.

How does it integrate with Pyth?

  • On-chain: The Plutus V3 validator reads ADA/USD prices directly from Pyth Lazer withdrawal redeemers. It locates the Pyth State UTxO in reference inputs, extracts the withdraw script hash, then parses the binary price payload (feed ID 16, ADA/USD) from the corresponding withdrawal redeemer. This is a full on-chain Pyth Lazer integration in Scalus (Scala-to-Plutus compiler).
  • Off-chain: The backend fetches signed price updates via the Pyth Lazer REST API (solana format) and includes them as withdrawal redeemers in every price-dependent transaction (open, borrow, liquidate).

What problem does it solve or demonstrate?

Demonstrates a complete DeFi lending protocol on Cardano powered by Pyth Lazer price feeds — from on-chain binary payload parsing to off-chain transaction building to a web frontend with wallet integration.

Directory Structure

lazer/cardano/pythacoin/
├── src/main/scala/pythacoin/onchain/  # Plutus V3 validator (Scalus)
├── src/main/scala/pythacoin/          # Backend: REST API, tx builder
├── src/test/scala/pythacoin/          # Unit tests (11 passing)
├── integration/                       # Integration tests (preprod)
├── frontend/                          # React 19 + TypeScript + Tailwind
├── build.sbt                          # Scala build config
├── flake.nix                          # Nix dev environment
└── README.md

Testing & Verification

How to Test This Contribution

Prerequisites

  • Nix (recommended) or JDK 21 + sbt + Node.js 20+
  • Blockfrost API key (preprod)
  • Pyth Lazer API key

Setup & Run Instructions

cd lazer/cardano/pythacoin

# If you have Nix, just run `nix develop` to get all dependencies

# Backend
sbt "runMain pythacoin.main start"

# Tests
sbt test

# Frontend
cd frontend && npm install && npm run dev

Deployment Information

Network: Cardano PreProd
Demo: https://youtu.be/RK7WsZQiG54

Checklist

Code Quality

  • Code follows existing patterns in the repository
  • Proper error handling implemented
  • No hardcoded values (use environment variables where appropriate)

Testing

  • Tested locally and works as expected
  • All existing functionality still works (no breaking changes)

Additional Context

Screenshots/Demo

Screenshot 2026-03-23 at 00 16 38

Demo video: https://youtu.be/RK7WsZQiG54

Notes for Reviewers

  • Built with Scalus (Scala-to-Plutus compiler) — first Pyth Lazer integration in Scala/Scalus
  • Full on-chain Pyth Lazer binary parsing (no Aiken dependency)
  • 11 unit tests + preprod integration test passing
  • Apache 2.0 licensed

Open with Devin

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

case _ => throw RuntimeException("Owner address must have a key credential")
case _ => throw RuntimeException("Owner address must be a Shelley address")
Log.info(s"Owner PKH: ${ownerPkh.hash.toHex}")
val nftName = AssetName(ByteString.fromString("CDP-" + System.currentTimeMillis()))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 CDP NFT name uses System.currentTimeMillis() instead of UTxO-derived hash, risking name collisions

The server generates CDP NFT names using ByteString.fromString("CDP-" + System.currentTimeMillis()), but the docstring at CdpTransactions.scala:38-39 specifies names should be "derived from sha2_256(firstInput.txId ++ firstInput.index)". The timestamp approach is not guaranteed unique — two concurrent Open CDP requests arriving at the same millisecond produce identical NFT names. This creates duplicate tokens that break CDP identification: CdpQueries.findCdpUtxo (CdpQueries.scala:37-46) returns the first matching UTxO, so subsequent operations (borrow, repay, close, liquidate) could be applied to the wrong CDP. The on-chain validator checks owner signatures for borrow/repay/close (mitigating those), but the Liquidate handler at CdpValidator.scala:278-299 requires no owner signature, so a liquidation could target the wrong CDP if both share a name and one is above the 90% LTV threshold.

Prompt for agents
In lazer/cardano/pythacoin/src/main/scala/pythacoin/Server.scala line 202, replace the timestamp-based NFT name generation with a UTxO-derived approach as documented in CdpTransactions.scala. The fix should:

1. First query the owner's UTxOs to find one that will be consumed as a transaction input
2. Derive the NFT name from a hash of that UTxO's transaction ID and index: sha2_256(txId ++ index)
3. Use this deterministic, unique name as the AssetName

For example:
  val ownerUtxos = ctx.provider.findUtxos(ownerAddr).await(30.seconds).getOrElse(throw ...)
  val firstInput = ownerUtxos.head._1
  val uniqueBytes = java.security.MessageDigest.getInstance("SHA-256").digest(
    firstInput.transactionId.bytes ++ BigInt(firstInput.index).toByteArray
  )
  val nftName = AssetName(ByteString.unsafeFromArray(uniqueBytes))

This ensures each CDP gets a globally unique NFT name derived from the consumed UTxO, matching the documented behavior at CdpTransactions.scala:38-39.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant