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
81 changes: 81 additions & 0 deletions lazer/cardano/hermes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Team "Los Hydra Boys" Pythathon Submission

## Details

Team Name: Los Hydra Boys
Submission Name: Hermes Market
Team Members: Ignacio Dopazo (@ignaciodopazo), Nicolás Ludueña (@nicolasLuduena), Valentino Cerutti (@Micrograx)
Contact: luduena.nicolas.victorio@gmail.com

## Project Description

Hermes market is a proof of concept integration of Pyth oracle information with Cardano's bleeding edge L2 [Hydra](https://hydra.family/head-protocol) that allows for real time bets on BTC price. Therefore truly giving an edge over a simple 5 minute market with up to 2 minute block finality in the L1 on the worst cases. Even on mainnet with high congestion scenarios, block confirmation is not guaranteed after several days.

## Motivation

Cardano is a powerful and very secure blockchain, but slow for the fast transactions: Cardano's consensus algorithm, while efficient, still requires massive global replication of state changes, potentially increasing transaction settlement times during peak hours. For our use case, we require real-time transaction settlement times.

For solving this issue, we rely on Hydra L2 to provide the speed and scalability we need for this project. Hydra L2 is a layer 2 solution that is built on top of Cardano and provides a low-latency, high-throughput, and secure way to process transactions.

For the Oracle service, we use Pyth, a service provider of real-time price feeds for various kinds of assets. For this PoC, we built a 5-minute prediction market for BTC/USD trading pair.

## Architecture

### Onchain

We designed an Aiken smart contract for managing the markets lifecycle and the bets placed on them through the orders script.

There are several operations that can be performed on the market script:
- Create a new market: every 5 minutes, a new market UTxOis created associated with a fresh order script.
- Place Buy or Sell order: a user can place a buy or sell order for receiving the appropriate position tokens.
- Record the final price of the market: the oracle service will record the final price in the market UTxO.
- Claim winnings: a winning user can claim the winnings by providing the position tokens.
- Claim losses: user provide their losing tokens for being burned.
- Close the market: the market is closed by burning the Market UTxO control token.
- Process an orders match: grab complementary orders from opposite directions, and distribute the tokens appropriately.

You can find the diagrams for all these transactions [here](./doc/transactions.pdf)

### Infrastructure

Two Hydra nodes required for running one Hydra Head in offline mode. We declare the initial UTxO set for the Hydra chain, inject the appropriate scripts and assets for working with the Pyth oracle service.

### Server & Offchain

The server encompasses the prediction market business logic for interacting with the onchain scripts, and with the offchain services that interact with the Pyth oracle service and builds the Market script transactions. Also, we have a Hydra service that connects seamlessly with the Hydra nodes for sending and receiving transactions, and looking up for useful blockchain information.

### Client

The client is a simple React application to visualize the current prediction market state, with real-time updates coming from the server being shown in a nice real-time udpated chart, with buy and sell orders being shown as well. It also allows to place buy and sell orders, and to claim winnings or losses.

### Limitations

A key limitation for this PoC is that we use Hydra in offline mode, which means that the Hydra nodes are not connected to a Cardano Node, but we just declar a initial UTxO set for the Hydra chain. For using Pyth within Hydra in this offline mode setup, we "inject" the Pyth-related onchain entities (withdraw validator and Pyth State asset) in the initial UTxO set.

But for a production deployment in a public testnet or mainnet, the part of injecting the Pyth-related onchain entities on the Hydra initial UTxO set presents a real challenge. We would need to spend the UTxO containing the Pyth State asset for being commited to the Hydra chain. Naturally, the Pyth contract will not allow this to happen.

So for this idea to work in a public testnet or mainnet, either an update of the Pyth contract is needed, or an update of the Hydra protocol is needed for supporting L1 UTxOs as reference inputs (no spending allowed) inside the Hydra Head. This would be sufficient since the UTxO holding the Pyth State asset is only needed as reference inputs in the Oracle operations.

## Tech Stack

- App: Vite + React + TailwindCSS
- Server: Node.js + WebSocket
- Infrastructure: Docker compose, Hydra
- Onchain: Aiken, [Pyth Lazer Aiken lib](https://github.com/pyth-network/pyth-lazer-cardano)
- Offchain: Lucid Evolution

## Setup & Run

1. Run the infrastructure: see [infra/README.md](./infra/README.md)
2. Run the server
```bash
cd server
pnpm install
pnpm api
```
3. Run the UI
```bash
cd ui
pnpm install
pnpm dev
```
229 changes: 229 additions & 0 deletions lazer/cardano/hermes/doc/report.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// Transaction diagrams

#let tx_out_height_estimate(input) = {
let address = if "address" in input { 1 } else { 0 }
let value = if "value" in input { input.value.len() } else { 0 }
let datum = if "datum" in input { input.datum.len() } else { 0 }
return (address + value + datum) * 8pt
}

#let datum_field(indent, k, val) = [
#if val == "" [
#h(indent)\+ #raw(k)
] else [
#h(indent)\+ #raw(k):
#if type(val) == content { val }
#if type(val) == str and val != "" { repr(val) }
#if type(val) == int { repr(val) }
#if type(val) == array [
#stack(dir: ttb, spacing: 0.4em, for item in val [
#datum_field(indent + 1.2em, "", item) \
])
]
#if type(val) == dictionary [
#v(-0.7em)
#stack(dir: ttb, spacing: 0em, for (k, v) in val.pairs() [
#datum_field(indent + 1.2em, k, v) \
])
]
]
]

#let tx_out(input, position, inputHeight) = {
let address = if "address" in input [
*Address: #h(0.5em) #input.address*
] else []
let value = if "value" in input [
*Value:* #if ("ada" in input.value) [ *#input.value.ada* ADA ] \
#v(-1.0em)
#stack(dir: ttb, spacing: 0.4em, ..input
.value
.pairs()
.map(((k, v)) => [
#if k != "ada" [
#h(2.3em) \+
#if type(v) == content { math.bold(v) }
#if type(v) == str and v != "" [*#v*]
#k
]
]))
] else []
let datum = if "datum" in input [
*Datum:* \
#v(-0.8em)
#stack(dir: ttb, spacing: 0.4em, ..input.datum.pairs().map(((k, val)) => datum_field(1.2em, k, val)))
] else []
let addressHeight = measure(address).height + if "address" in input { 6pt } else { 0pt }
let valueHeight = measure(value).height + if "value" in input { 6pt } else { 0pt }
let datumHeight = measure(datum).height + if "datum" in input { 6pt } else { 0pt }
let thisHeight = 32pt + addressHeight + valueHeight + datumHeight

if "dots" in input {
return (
content: place(dx: position.x, dy: position.y, [
#place(dx: 4em, dy: -1em)[*.*]
#place(dx: 4em, dy: 0em)[*.*]
#place(dx: 4em, dy: 1em)[*.*]
]),
height: thisHeight,
)
} else {
return (
content: place(dx: position.x, dy: position.y, [
*#input.name*
#line(start: (-4em, -1em), end: (10em, -1em), stroke: red)
#place(dx: 10em, dy: -1.5em)[#circle(radius: 0.5em, fill: white, stroke: red)]
#if "address" in input { place(dx: 0em, dy: -3pt)[#address] }
#place(dx: 0em, dy: addressHeight)[#value]
#if "datum" in input { place(dx: 0em, dy: addressHeight + valueHeight)[#datum] }
]),
height: thisHeight,
)
}
}

#let vanilla_transaction(
name,
inputs: (),
outputs: (),
signatures: (),
certificates: (),
withdrawals: (),
mint: (:),
validRange: none,
notes: none,
) = context {
let inputHeightEstimate = inputs.fold(0pt, (sum, input) => sum + tx_out_height_estimate(input))
let inputHeight = 0em
let inputs = [
#let start = (x: -18em, y: 1em)
#for input in inputs {
let tx_out = tx_out(input, start, inputHeight)

tx_out.content

// Now connect this output to the transaction
if not "dots" in input {
place(dx: start.x + 10.5em, dy: start.y + 0.84em)[
#let lineStroke = if input.at("reference", default: false) {
(paint: blue, thickness: 1pt, dash: "dashed")
} else { blue }
#line(start: (0em, 0em), end: (7.44em, 0em), stroke: lineStroke)
]
place(dx: start.x + 10.26em, dy: start.y + 0.59em)[#circle(radius: 0.25em, fill: blue)]
}
if input.at("redeemer", default: none) != none {
place(dx: start.x + 12.26em, dy: start.y - 0.2em)[#input.at("redeemer")]
}

start = (x: start.x, y: start.y + tx_out.height)
inputHeight += tx_out.height
}
]

let outputHeightEstimate = outputs.fold(0pt, (sum, output) => sum + tx_out_height_estimate(output))
let outputHeight = 0em
let outputs = [
#let start = (x: 4em, y: 1em)
#for output in outputs {
let tx_out = tx_out(output, start, outputHeight)
tx_out.content
start = (x: start.x, y: start.y + tx_out.height)
outputHeight += tx_out.height
}
]

// Collapse down the `mint` array
let display_mint = (:)
for (k, v) in mint {
let display = []
if type(v) == int {
// the provided value is an integer
if v == 0 {
continue
} else if v > 0 {
display += [ \+ ]
} else if v < 0 {
display += [ \- ]
}
display += [#calc.abs(v)]
} else {
// the provided value can be a letter or variable name
if type(v) == str and v.starts-with("-") {
display += [\- #v.slice(1)]
} else {
display += [\+ #v]
}
}
display += [ #raw(k)]
display_mint.insert(k, display)
}

let mints = if display_mint.len() > 0 [
*Mint:* \
#for (k, v) in display_mint [
#v \
]
] else []
let sigs = if signatures.len() > 0 [
*Signatures:* \
#for signature in signatures [
- #signature
]
] else []
let certs = if certificates.len() > 0 [
*Certificates:*
#for certificate in certificates [
- #certificate
]
] else []
let withs = if withdrawals.len() > 0 [
*Withdrawals:*
#for withdrawal in withdrawals [
- #withdrawal
]
] else []
let valid_range = if validRange != none [
*Valid Range:* \
#if "lower" in validRange [#validRange.lower $<=$ ]
`slot`
#if "upper" in validRange [$<=$ #validRange.upper]
] else []

let boxHeight = {
100pt + 32pt * (mint.len() + certificates.len() + signatures.len()) + 40pt * withdrawals.len()
}

let transaction = [
#set align(center)
#rect(
radius: 4pt,
height: calc.max(boxHeight, inputHeight + 16pt, outputHeight + 16pt),
[
#pad(top: 1em, name)
#v(1em)
#set align(left)
#stack(dir: ttb, spacing: 1em, mints, sigs, certs, withs, valid_range)
],
)
]

let diagram = stack(dir: ltr, inputs, transaction, outputs)
let size = measure(diagram)
block(width: 100%)[
#set align(center)
#diagram
#set align(left)
#if notes != none [
#box(
width: 100%,
fill: rgb("f0f0f0"),
stroke: 0.5pt + black,
inset: 8pt,
)[
#set text(size: 9pt)
*Notes*: #notes
]
]
]
}
Binary file added lazer/cardano/hermes/doc/transactions.pdf
Binary file not shown.
Loading