Kodrat is a Solana Devnet hackathon demo, but the full quote and purchase flow requires running the RFQ market-maker bot locally. A deployed frontend by itself can show the UI, but it cannot request signed option quotes unless BOT_URL points to a running bot.
Run locally:
cd option_protocol/bot
set -a; . /tmp/kodrat-devnet.env; set +a
cargo run --releasecd option_protocol/frontend
npm install
npm run dev -- --host 127.0.0.1Then open http://127.0.0.1:3000/app.html, connect a Devnet wallet, request a quote, and buy an option. The current demo uses the Devnet program H5yv1n2BMwPXgVGYA9Rewz4PQFx7p3RDpbZgBajoiULY, demo USDC mint HaeZjxpic6AWcd6aS2TfLzHXq1c4qFegR6WMd5aH5CRv, and a locally hosted bot at http://localhost:8787.
Do not commit /tmp/kodrat-devnet.env or local .kodrat-mm-signer-*.hex files; they contain demo market-maker signer material.
Hackathon demo on Solana Devnet. All collateral is a program-owned demo stablecoin (demoUSD). No real funds are at risk.
Meridian FX lets retail users lock in a USD/IDR or USD/JPY exchange rate for a fixed horizon, and lets liquidity providers earn the spread by taking the other side.
Emerging-market exporters and importers face FX rate risk between invoice and payment. Forward contracts exist but require bank relationships, minimum notionals, and KYC. Meridian FX provides the same economic exposure on-chain:
- Traders deposit USDC (demoUSD on devnet), pick a direction, and receive a guaranteed settlement rate if they hold to expiry.
- LPs deposit into a pool to backstop positions. They earn the spread on every trade plus collect losing traders' collateral at settlement.
This is not a bilateral NDF with a forward curve and tenor pricing. Economically it is a full-collateral, capped FX bet priced at spot ± a static spread. Think of it as the simplest on-chain equivalent of a deliverable forward.
┌────────────────────────────────────────────────────────┐
│ Anchor Program (86v5K13XU2TiFmg2HCf8sHf9TYGXsaoBGM1UEFZhoXer) │
│ │
│ Pool ──── PriceFeed (base) ── PriceFeed (quote) │
│ │ │
│ ├── LpAccount (one per LP) │
│ ├── Position (one per trade) │
│ └── PoolVault (SPL token account) │
└────────────────────────────────────────────────────────┘
| Account | Seeds | Description |
|---|---|---|
Pool |
[b"pool", pair, expiry_le] |
Market for one (pair, expiry) |
PoolVault |
[b"pool_vault", pool] |
SPL token account holding all collateral |
LpAccount |
[b"lp_account", pool, lp] |
LP shares for one wallet |
Position |
[b"pos", pool, trader, nonce] |
One open trade |
PriceFeed |
[b"price_feed", symbol_8b] |
Admin-maintained oracle |
DemoMint |
[b"demo_mint"] |
Program-owned stablecoin (devnet only) |
| Instruction | Who calls it | What it does |
|---|---|---|
init_pool |
admin | Create a (pair, expiry) market with a spread |
deposit_liquidity |
LP | Mint shares priced at current NAV |
withdraw_liquidity |
LP | Burn shares, receive total_usdc × shares / supply |
open_position |
trader | Lock collateral, record entry rate |
settle_pool |
anyone | Capture settlement rate after expiry |
settle_position |
anyone | Pay out one position based on settlement rate |
update_price_feed |
admin | Push a new price for a symbol |
init_demo_mint |
admin | One-time demo mint setup |
airdrop_demo_usdc |
anyone | Mint up to 1,000 demoUSD per call |
mid_rate = base_usd_price / quote_usd_price (6 dp)
entry = mid × (1 ± spread_bps / 10_000)
pnl = notional × |settlement − entry| / entry (capped at notional)
payout = notional + pnl (win) or notional − pnl (loss)
LP NAV moves inversely to net trader PnL: when traders collectively profit, LP shares lose value; when traders lose, LP NAV rises.
| Route | Purpose |
|---|---|
/ |
Market overview — active pools with liquidity and utilization |
/hedge |
Retail flow — pick use case, amount, expiry date, submit |
/lp |
LP dashboard — deposit/withdraw per pool |
/portfolio |
Open and settled positions, claim payout |
The frontend requires no wallet for the market overview (read-only). Trades, LP actions, and airdrop require a connected Solana wallet (Phantom, Backpack, etc.) on Devnet.
The following are known issues that would need to be resolved before a production deployment:
Security
init_poolaccepts any signer as admin; there is no protocol-level registry of trusted markets. A malicious user can create lookalike pools on devnet.open_positionandsettle_poolaccept arbitraryPriceFeedaccounts without verifying the symbol againstpool.pair. An attacker can supply a freshly-initialized feed at an arbitrary price. In production, feeds should be validated against a trusted registry.update_price_feedinitializes a new feed on first call from any signer, so anyone can create their own price authority.
Economics
- LP NAV pricing includes trader collateral in
total_usdc. This means share prices are overstated while positions are open, and NAV drops sharply at settlement when losing traders' collateral is paid out. A proper implementation would separate LP capital from trader collateral. reserved_usdctracks pool-side collateral.total_usdc − reserved_usdcis the withdrawal ceiling for LPs. However,total_usdcalso includes trader collateral posted asopen_positiondeposits, so LPs can theoretically withdraw funds that implicitly back active positions. This is safe in normal cases (pool-side reserve > trader notional) but creates a solvency edge case if pool NAV drops significantly.- Spread is a fixed 30 bps regardless of tenor, utilization, or skew. An EM FX tail-risk book needs dynamic pricing.
- Tiny deposits/withdrawals can round to zero shares or zero USDC (integer truncation). Minimum size is not enforced.
UX / liveness
- Oracle freshness: the program requires price updates within 60 seconds. Without an automated keeper, trades will fail on a cold devnet. Run
npm run update-feedsor use the admin panel before demoing. - Settlement requires a two-step flow: first
settle_pool(capture rate), thensettle_positionper trade. The app's Portfolio page shows unsettled positions but does not yet provide a "Settle pool" admin button. - Displayed preview rate comes from a third-party REST API; on-chain entry rate comes from the
PriceFeedaccount. These can diverge if the feed is stale.
ndf_protocol/
├── programs/ndf_protocol/ Anchor program (Rust)
│ ├── src/instructions/ One file per instruction
│ ├── src/math.rs Pure arithmetic (30+ unit tests)
│ ├── src/state.rs Account layouts
│ └── src/errors.rs Error codes
├── tests/integration.rs 6 end-to-end tests via litesvm
├── app/ Next.js 14 frontend
│ ├── src/pages/ Route pages
│ ├── src/components/ NavBar, AirdropButton, etc.
│ ├── src/hooks/ useNdfProgram, usePools, useDemoMint, …
│ ├── src/lib/ IDL, constants, math utils
│ └── scripts/init_markets.ts Admin bootstrap script
└── Anchor.toml
- Rust +
cargo(stable) - Solana CLI ≥ 1.18
- Anchor CLI 1.0.x (
avm use 1.0.x) - Node.js ≥ 18, yarn
cd ndf_protocol
cargo test # 33 unit tests + 6 integration tests
anchor build # compile to target/deploy/cd ndf_protocol/app
yarn install
yarn dev # http://localhost:3000# Requires ~/.config/solana/id.json funded on devnet
cd ndf_protocol/app
npm run init-markets # creates demo mint + 8 pools (USD/IDR × 4, USD/JPY × 4)After seeding, keep a keeper running to refresh oracle prices:
npm run update-feeds # push fresh prices every ~30stests/integration.rs
├── admin_can_init_pool happy-path pool creation
├── lp_deposit_increases_tvl deposit → TVL, shares, NAV
├── trader_open_position_happy_path open → collateral locked, reserved_usdc rises
├── lp_deposit_withdraw_roundtrip full roundtrip, no positions → get principal back
├── short_position_profits_when_idr_appreciates direction=1, rate falls → payout > notional
└── cannot_double_settle_position PositionAlreadySettled on second settle
All tests use litesvm for sub-second execution without a validator.
MIT