Skip to content

ametel01/horizon-starknet

Repository files navigation

Horizon Protocol

Horizon Protocol

A Pendle-style yield tokenization protocol for Starknet — enabling users to separate yield-bearing assets into principal and yield tokens, unlocking advanced DeFi strategies.

Build Test Format

Quick Links

Status & Coverage

Property Value
Status Alpha (Mainnet live)
Network Starknet Mainnet
Safety Model Non-custodial; mixed upgradeability (see below)
License Business Source License 1.1 (converts to GPL-3.0 on 2028-12-19)

Quick Facts (Security & Integration Triage)

Property Status
Upgradeable Yes: All core contracts (SY, PT, YT, Market, Factory, MarketFactory, Router) are owner-upgradeable; only Mocks and Faucet are immutable
Admin Keys Owner-controlled upgrades on all core contracts; no pause functions; no parameter controls
Audits Not yet audited (Alpha stage)
Bug Bounty Not yet active (TBD)
Key Dependencies Starknet RPC, Pragma oracle (TWAP price feeds), hrzSTRK (mock staked token for testing)
Live Addresses See Deployments section

Overview

Horizon splits yield-bearing assets into two components:

  • Principal: The base value of your deposit
  • Yield: The future interest/rewards your deposit will generate

This separation enables powerful DeFi strategies like locking in fixed yields, speculating on yield rates, or hedging yield exposure.

Core Concepts

SY (Standardized Yield)

Wrapper tokens that standardize any yield-bearing asset (staking derivatives, lending positions, LP tokens) into a common interface. SY tokens track the exchange rate between the wrapper and underlying asset.

PT (Principal Token)

Represents the principal portion of a yield-bearing asset. PT holders can redeem 1:1 for the underlying SY at expiry. Before expiry, PT trades at a discount reflecting the time value of money (implied yield rate).

YT (Yield Token)

Represents the yield portion. YT holders receive all yield generated by the underlying asset until expiry. After expiry, YT becomes worthless as no more yield accrues.

Market (AMM)

A specialized AMM for trading PT against SY. Uses a time-decay pricing curve where PT price converges to 1:1 with SY as expiry approaches. The market discovers the implied yield rate through trading activity.

Architecture

┌─────────────────┐
│ Underlying Asset│  (e.g., stETH, aUSDC)
└────────┬────────┘
         │ deposit
         ▼
┌─────────────────┐
│       SY        │  Standardized Yield wrapper
└────────┬────────┘
         │ mint_py
         ▼
┌─────────────────┐
│    PT + YT      │  Split into Principal + Yield
└────────┬────────┘
         │
    ┌────┴────┐
    ▼         ▼
┌───────┐ ┌───────┐
│ Market│ │ Hold  │  Trade PT/SY or hold for yield
└───────┘ └───────┘

User Flows

Fixed Yield Strategy

  1. Deposit underlying → Get SY
  2. Mint PT + YT from SY
  3. Sell YT on secondary market
  4. Hold PT until expiry → Redeem for guaranteed principal

Yield Speculation

  1. Buy YT tokens from market
  2. Collect yield as it accrues via redeem_due_interest()
  3. Profit if actual yield > price paid for YT

Liquidity Provision

  1. Deposit SY + PT into Market
  2. Earn trading fees from swaps
  3. Remove liquidity anytime

Contracts & Deployments

Contract Roles

Contract Description
SY ERC20 wrapper that standardizes yield-bearing assets
PT Principal token, redeemable 1:1 at expiry
YT Yield token, accrues interest until expiry
Market AMM with time-weighted pricing for PT/SY trading
Factory Deploys SY/PT/YT token sets for new underlying assets
MarketFactory Deploys markets for PT tokens
Router Aggregates operations with slippage protection

Mainnet Deployment (Starknet Mainnet)

Contract Address Upgradeable Notes
Factory 0x04fd6d42072f76612ae0a5f97d191ab4c5ede3688d2df0185352e01b7f2fc444 Yes Deploys SY/PT/YT pairs; owner-upgradeable
MarketFactory 0x0465bc423ddde2495e9d4c31563e0b333d9c8b818a86d3d76064fd652ee4be6f Yes Deploys PT/SY AMM markets; owner-upgradeable
Router 0x07ccd371e51703e562cf7c7789d4252b7a63845dc87f25a07cf8b5c28e80563b Yes User entry point; owner-upgradeable

hrzSTRK Market (Mock Staked STRK)

Contract Address Upgradeable Purpose
hrzSTRK 0x02f87c98336c7457aa5aa9237d41e2b903346aeae64be60f63423486d0c16f02 No (mock) ERC-4626 mock yield token (staking vault)
Faucet 0x01b472105605d569778515bcf7ea03759f77af0b2cc945dc999a19edc6572d71 No Test token distributor
SY (hrzSTRK) 0x0601a6717bedf8010f68ec2e4993ea12c208ed949ed76b33b616add725dbc15c Yes Standardized yield wrapper
PT (hrzSTRK) 0x05f94e491f3deb3137eecc94a4641f11112219e7923613d2e2f0f3ef5496b9ca Yes Principal token, redeemable 1:1 at expiry
YT (hrzSTRK) 0x070c396667613d74cb473ad937d717355222c76a41d3a1d2b34299eefda6405d Yes Yield token, accrues interest until expiry
Market (hrzSTRK/SY) 0x04d2f052b91f5c744e67816e72e426cab538661deda0d38143a9cefad973c18 Yes PT/SY AMM trading pool

Deployment Info:

  • Network: Starknet Mainnet
  • Deployed: 2025-12-23
  • Expiry: January 17, 2026 (Unix timestamp: 1774018125)
  • Deployment: ./deploy/scripts/deploy.sh mainnet
  • Explorer: Use Starkscan to verify addresses
  • Verification: Run scarb build with Scarb 2.15.0 and compare bytecode (see Reproducible Build)

Key Functions

Router (recommended entry point)

// Mint PT + YT from SY tokens
fn mint_py_from_sy(yt, receiver, sy_amount, min_py_out) -> (pt_out, yt_out)

// Redeem PT + YT back to SY (before expiry)
fn redeem_py_to_sy(yt, receiver, py_amount, min_sy_out) -> sy_out

// Redeem PT only (after expiry)
fn redeem_pt_post_expiry(yt, receiver, pt_amount, min_sy_out) -> sy_out

// Swap SY for PT
fn buy_pt_from_sy(market, receiver, sy_in, min_pt_out) -> pt_out

// Swap PT for SY
fn sell_pt_for_sy(market, receiver, pt_in, min_sy_out) -> sy_out

// Add liquidity to market
fn add_liquidity(market, receiver, sy_desired, pt_desired, min_lp_out) -> lp_out

// Remove liquidity from market
fn remove_liquidity(market, receiver, lp_amount, min_sy_out, min_pt_out) -> (sy_out, pt_out)

Market Mechanics

The Market uses a modified AMM curve that accounts for time until expiry:

  • Rate Scalar: Controls price sensitivity to reserve changes
  • Anchor: Initial implied rate setting
  • Fee Rate: Trading fee percentage

As expiry approaches:

  • PT price converges toward 1:1 with SY
  • Implied rate becomes more volatile with smaller trades
  • At expiry, PT = SY in value

Developer

Prerequisites (pinned versions)

Use asdf or your OS package manager to install exact versions:

# Verify installations
scarb --version     # Should show 2.15.0
snforge --version   # Should show 0.54.1
starkli --version   # Should show 0.4.2

Install & Build

cd contracts
scarb build

Test

# Run all tests
cd contracts && snforge test

# Run with backtrace on failure
cd contracts && SNFORGE_BACKTRACE=1 snforge test

# Run specific test
cd contracts && snforge test test_full_flow

Format & Lint

cd contracts && scarb fmt

Reproducible Build

To verify bytecode matches on-chain deployments:

cd contracts
scarb clean
scarb build
# Compare CASM/Sierra artifacts in target/ with on-chain code
# See deployment tag/commit at https://github.com/ametel01/horizon-starknet/releases

Local Development (Docker)

# Start local devnet + deploy contracts
make dev-up

# View logs
make dev-logs

# Stop devnet
make dev-down

# Fork mainnet (uses real Pragma oracle)
make dev-fork
make dev-fork-down

Example: Deploy to local devnet

./deploy/scripts/deploy.sh devnet
./deploy/scripts/export-addresses.sh devnet  # Export to JSON

Project Structure

├── contracts/
│   ├── src/
│   │   ├── factory.cairo        # Deploys SY/PT/YT pairs
│   │   ├── router.cairo         # User entry point with slippage protection
│   │   ├── tokens/              # SY, PT, YT implementations
│   │   ├── market/              # AMM and MarketFactory
│   │   ├── libraries/           # math.cairo (WAD), market_math.cairo, errors, roles
│   │   ├── interfaces/          # Contract interfaces (ISY, IPT, IYT, IMarket, etc.)
│   │   ├── oracles/             # Pragma oracle integration
│   │   └── mocks/               # Test mocks (MockYieldToken, MockPragma, Faucet)
│   └── tests/                   # Unit & integration tests
├── packages/
│   ├── frontend/                # Next.js dApp (Bun, React 19, TailwindCSS 4)
│   └── indexer/                 # Event indexer (Bun, Apibara DNA, PostgreSQL)
├── deploy/
│   ├── scripts/                 # deploy.sh, declare.sh, export-addresses.sh
│   ├── addresses/               # Deployed addresses (devnet.json, mainnet.json)
│   └── accounts/                # sncast account files
└── .github/workflows/           # CI pipelines (build, test, fmt)

Indexer

The indexer captures all protocol events from Starknet and stores them in PostgreSQL for analytics and frontend queries.

Architecture

  • Runtime: Bun + Apibara DNA (Starknet indexing framework)
  • Database: PostgreSQL 16 (hosted on Railway)
  • Tables: 40 event tables + 23 views (9 materialized, 14 enriched/aggregated)

Local Development

cd packages/indexer

# Install dependencies
bun install

# Start local infrastructure (PostgreSQL + DNA server)
bun run docker:up

# Run indexer with devnet preset
bun run dev

# Run with mainnet preset
bun run dev:mainnet

# Run tests
bun run test

# Type check + lint + format
bun run check

Database Commands

# Generate migrations after schema changes
bun run db:generate

# Push schema directly (dev only)
bun run db:push

# Open Drizzle Studio (database GUI)
bun run db:studio

# Create/refresh materialized views
bun run db:create-views
bun run db:refresh-views

Remote Database Access

The production indexer runs on Railway with a PostgreSQL database.

# Install Railway CLI (one-time setup)
bun add -g @railway/cli

# Login to Railway (opens browser for authentication)
~/.bun/bin/railway login

# Link to the indexer project (select from list)
cd packages/indexer
~/.bun/bin/railway link

# Connect to PostgreSQL console
~/.bun/bin/railway connect Postgres

Once connected, you can run SQL queries directly. For example, to check table row counts:

SELECT 'router_swap' as table_name, COUNT(*) FROM router_swap
UNION ALL
SELECT 'market_swap', COUNT(*) FROM market_swap;

Key Tables

Table Description
factory_yield_contracts_created SY/PT/YT pair deployments
market_factory_market_created Market deployments
router_swap All swap transactions
router_add_liquidity / router_remove_liquidity LP operations
market_implied_rate_updated Implied rate changes
sy_deposit / sy_redeem SY token operations
yt_interest_claimed Yield claims

Views

Analytics views are refreshed periodically for dashboard queries:

  • 9 Materialized views: Daily stats, current state, user positions
  • 14 Enriched/aggregated views: Router events with token metadata, SY monitoring, YT interest analytics
  • Example views: market_stats, user_positions, yield_history

Security

Audits

  • Current Status: Not yet audited (Alpha stage, live on mainnet)
  • Planned: Security audit in progress / scheduled post-launch

Bug Bounty

  • Status: Not yet active
  • Info: Will be announced at launch

Threat Model & Known Risks

IMPORTANT: Horizon is in Alpha on mainnet with real funds at risk. Carefully evaluate your risk tolerance.

Smart Contract Risks

  • Precision Loss: All arithmetic uses WAD (10^18) fixed-point math; extreme precision inputs may exhibit rounding
  • Oracle Risk: PT/YT pricing depends on accurate yield rate discovery via market AMM; illiquid markets may have poor price discovery
  • Expiry Handling: At maturity, YT becomes worthless and PT redeems to underlying; ensure frontend clearly communicates expiry dates
  • Flash Loan Risk: Not currently guarded against flash loan exploits in initial release; monitor governance decisions
  • Upgrade Risk: All core contracts are upgradeable; future upgrades could alter behavior or introduce bugs

Protocol-Specific Risks

  • Yield-Bearing Asset Risk: Underlying assets (stETH, aUSDC, etc.) carry their own smart contract, oracle, and liquidation risks
  • Liquidation Risk: Users holding PT into maturity lock in time value; PT value floor is underlying asset's accounting value
  • Access Control: Minting/burning PT/YT restricted to YT contract only; ensure YT contract itself is secure
  • Time Decay: Market AMM uses time-based pricing; significant price swings expected near expiry

Infrastructure Risks

  • Starknet RPC: Protocol depends on Starknet L2 RPC availability; network outages prevent state updates
  • Pragma Oracle: Price feeds sourced from Pragma; oracle downtime affects market pricing
  • Bridge Risk: If using cross-chain yield assets, inherits bridge security assumptions

Asset-Specific Risks

  • Non-Standard Tokens: Protocol assumes ERC20 standard; tokens with custom fee-on-transfer or rebasing mechanics untested
  • Decimals: Assets with non-18-decimal values may exhibit precision issues; validate before wrapping

Security Best Practices

  • All public functions have comprehensive unit + integration tests
  • Slippage protection enabled on all router operations
  • Expiry checks prevent minting PT/YT after maturity
  • Access control on PT/YT minting (only YT contract can mint/burn)
  • No pause/emergency functions
  • All core contracts are owner-upgradeable for bug fixes and improvements

Security Disclosure

Report security issues to: [TBD — add security contact before launch]

Expected response time: Within 48 hours of report.

Do not disclose vulnerabilities publicly until a fix is released.

Integration

Core Interfaces

The Router contract is the recommended entry point for most integrations:

// Mint PT + YT from SY
fn mint_py_from_sy(yt: ContractAddress, receiver: ContractAddress, sy_amount: u256, min_py_out: u256)

// Redeem PT + YT back to SY (pre-expiry)
fn redeem_py_to_sy(yt: ContractAddress, receiver: ContractAddress, py_amount: u256, min_sy_out: u256)

// Redeem PT only (post-expiry)
fn redeem_pt_post_expiry(yt: ContractAddress, receiver: ContractAddress, pt_amount: u256, min_sy_out: u256)

// Trade SY → PT
fn buy_pt_from_sy(market: ContractAddress, receiver: ContractAddress, sy_in: u256, min_pt_out: u256)

// Trade PT → SY
fn sell_pt_for_sy(market: ContractAddress, receiver: ContractAddress, pt_in: u256, min_sy_out: u256)

// Add liquidity (PT/SY pair)
fn add_liquidity(market: ContractAddress, receiver: ContractAddress, sy_desired: u256, pt_desired: u256, min_lp_out: u256)

// Remove liquidity
fn remove_liquidity(market: ContractAddress, receiver: ContractAddress, lp_amount: u256, min_sy_out: u256, min_pt_out: u256)

Key Events to Index

All state changes are emitted as events:

  • PYCreated(yt_address, sy_address, expiry, py_index)
  • PYMinted(yt, receiver, py_amount, sy_amount)
  • PYRedeemed(yt, receiver, py_amount, sy_out)
  • Swap(market, buyer, sell_token, sell_amount, buy_amount, swap_fee)
  • LiquidityAdded(market, lp_receiver, sy_in, pt_in, lp_out)
  • LiquidityRemoved(market, lp_provider, lp_amount, sy_out, pt_out)

Assumptions & Expectations

Property Value Notes
Decimals 18 (WAD) All token amounts use 18 decimals
Precision Fixed-point WAD math Rounding toward zero in divisions
Slippage Protection Required on router calls Caller must provide min_out parameters
Time Units Seconds (u64) Expiry expressed as Unix timestamp
RPC Health Dependency Protocol depends on Starknet RPC uptime
Oracle Freshness Pragma feeds May have staleness; check oracle timestamp

Example Integration Flow

// 1. User has SY tokens and wants to split into PT + YT
let (pt_out, yt_out) = router.mint_py_from_sy(yt, receiver, sy_amount, min_py_out);

// 2. User sells YT on secondary market, keeps PT for maturity
// (YT price = market discovery of yield rate)

// 3. At expiry, redeem PT for underlying value
let sy_out = router.redeem_pt_post_expiry(yt, receiver, pt_amount, 0);

Governance, Upgrades & Permissions

Upgradeability

All core protocol contracts are owner-upgradeable using OpenZeppelin's components:

Contract Authority Purpose
SY Owner Standardized yield wrapper logic
PT Owner Principal token redemption and expiry logic
YT Owner Yield token accrual logic
Market Owner AMM pricing curves and pool logic
Factory Owner May adjust deployment validation or add new features
MarketFactory Owner May optimize market creation or parameters
Router Owner May add new operations or improve integration

Non-upgradeable contracts: Mocks and Faucet (test infrastructure only)

Upgrade Process:

  • Only owner can invoke upgrade(new_class_hash)
  • No timelock currently (subject to governance review)
  • Upgrades are logged on-chain as events

Admin Controls

Owner privileges:

  • Owner can upgrade all core protocol contracts
  • No pause function; no fund freezing capability
  • No parameter changes affecting user funds outside of upgrades

Future Governance (Roadmap)

  • Governance forum: TBD (community discussion on upgrade policies)
  • Potential transition to multi-sig or timelock for future upgrades

License

Horizon is licensed under the Business Source License 1.1 (BUSL-1.1).

Aspect Details
Source Visibility Code is source-available under BUSL-1.1 restrictions until Change Date
Commercial Use Restricted; usage on Horizon-deployed instances allowed immediately
Change Date 2028-12-19
Change License Converts to GPL-3.0 on change date
Summary Source-available with competitive protection + automatic open-source transition

For details, see the LICENSE file.

About

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors