EpochFortune Documentation
The Dual Lottery
Complete documentation for EpochFortune on Cardano. Two lotteries, one platform: the No-Loss Prize Pool for patient stakers, and the Ticket Lottery for bold risk-takers.
Introduction
EpochFortune is a decentralized lottery platform built on Cardano, offering two distinct ways to play. Choose the strategy that matches your risk appetite.
Key Benefits
- Two lottery modes — choose your risk level
- Non-custodial — funds remain in verifiable smart contracts
- Fair winner selection using commit-reveal randomness
- Transparent and auditable on-chain
Available Games
Choose your strategy. Each game is documented in its own section below.
The No-Loss Principle
"You can only win, never lose."
The no-loss principle is the foundation of EpochFortune. It guarantees that participants can never lose their deposited funds. Your ADA remains yours — it is merely delegated to a stake pool on your behalf.
How It Works
Deposit
Your ADA goes into the pool
Stake
Pool earns ~3-4% APY rewards
Win/Safe
Win rewards or withdraw principal
Traditional Lottery vs. No-Loss Lottery
| Aspect | Traditional Lottery | EpochFortune |
|---|---|---|
| Risk | Lose ticket price if you don't win | Principal always safe |
| Prize Source | Ticket sales | Staking rewards only |
| Withdrawal | Tickets are spent | Full withdrawal anytime |
| Odds | Fixed by ticket count | Proportional to deposit |
| House Edge | Often 30-50% | 5.5% protocol fee only |
How It Works
1. Deposit
Users deposit a minimum of 10 ADA into the shared smart contract pool. Each deposit is recorded in the pool datum along with the participant's address and ticket weight. The deposit is converted to tickets using the calc_ticket_weight function.
2. Staking
The pool address is delegated to a Cardano stake pool, earning approximately 3-4% APY. These rewards accumulate over each epoch (5 days) and form the prize pot. The staking is handled by the pool_stake.ak validator which ensures only the admin can change delegation and rewards can only be withdrawn during a legitimate draw.
3. The Draw
Every epoch, the accumulated staking rewards are awarded to one winner. The winner is selected using a commit-reveal scheme that ensures tamper-resistant randomness. The more tickets you have (proportional to your deposit), the higher your chance of winning.
4. Withdrawal
Participants can withdraw their full deposit at any time. There is no lock-up period. Upon withdrawal, the participant is removed from the pool datum and receives their original deposit back in full.
Draw Frequency
Smart Contract Architecture
EpochFortune consists of five interconnected smart contracts written in Aiken (Plutus V3). Three contracts power the No-Loss Prize Pool, and two contracts power the Ticket Lottery.
┌─────────────────────────────────────────────────────────────────┐ │ No-Loss Prize Pool Architecture │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Users Smart Contracts Stake Pool │ │ ────── ─────────────── ────────── │ │ │ │ Deposit ──► pool_spend.ak pool1... │ │ Withdraw ◄── (State UTxO + ──► (earns ~3.5% APY) │ │ State NFT) ◄── Rewards │ │ │ │ │ │ pool_stake.ak ────────────► │ │ │ (certifying + │ │ │ rewarding) │ │ │ │ │ │ │ DrawWinner TX ◄─────── withdrawal() │ │ (commit-reveal) │ │ │ │ │ Winner (94.5%) + Protocol Fee (5.5%) │ │ │ │ pool_mint.ak: One-shot minting of State Thread NFT │ │ │ └─────────────────────────────────────────────────────────────────┘
No-Loss Prize Pool Contracts
| Contract | Type | Purpose |
|---|---|---|
pool_spend.ak | Spending Validator | Handles deposits, withdrawals, and draw execution |
pool_stake.ak | Stake Validator | Controls delegation and reward withdrawal |
pool_mint.ak | Minting Policy | Mints the unique State Thread NFT |
pool_spend.ak
The main spending validator that controls the pool's UTxO. It validates all operations that modify the pool state: deposits, withdrawals, commit draws, and winner selection.
Redeemers
| Redeemer | Description | Access |
|---|---|---|
Deposit | Add new participant to the pool | Any user |
Withdraw | Remove participant and return deposit | Participant only |
CommitDraw | Start commit-reveal randomness phase | Admin only |
DrawWinner | Execute draw and distribute rewards | Admin only |
Delegate | Update stake pool delegation | Admin only |
Deposit Validation
The validate_deposit function ensures:
- Minimum deposit of 10 ADA is met
- No duplicate deposits from the same address
- Ticket weight is calculated correctly
- Participant data is stored accurately
- Immutable fields remain unchanged
fn validate_deposit(
datum: PoolDatum,
out_datum: PoolDatum,
own_value: Value,
script_output: Output,
tx: Transaction,
) -> Bool {
let input_lovelace = ada_of(own_value)
let output_lovelace = ada_of(script_output.value)
let deposited = output_lovelace - input_lovelace
// Minimum deposit check
expect deposited >= min_deposit
// Calculate ticket weight
let ticket_weight = calc_ticket_weight(deposited)
// ... validation logic
}Withdrawal Validation
The validate_withdraw function ensures the participant receives their exact deposit amount and is properly removed from the pool.
DrawWinner Validation
The validate_draw_winner function implements the commit-reveal verification:
// Verify the commit hash: blake2b_256(secret <> salt) == draw_commit_hash
expect blake2b_256(bytearray.concat(secret, salt)) == datum.draw_commit_hash
// Validate minimum epoch delay between commit and draw
expect datum.current_epoch >= datum.commit_epoch + min_commit_delay_epochs
// Must have at least 2 participants for a fair draw
expect list.length(datum.participants) >= 2
// Derive randomness from secret, tx hash, and salt
let entropy = blake2b_256(
bytearray.concat(bytearray.concat(secret, tx.id), salt)
)pool_stake.ak
The stake validator controls delegation and reward withdrawal. It ensures rewards can only be withdrawn during a legitimate DrawWinner transaction.
Key Security Mechanism
The most important security feature is the coupling between reward withdrawal and the DrawWinner redeemer:
withdraw(redeemer: StakeRedeemer, _account: Credential, tx: Transaction) {
when redeemer is {
WithdrawRewards -> {
// Verify the spend script is also being executed in this transaction
// (i.e., DrawWinner redeemer is present)
let spend_script_cred = Script(spend_script_hash)
let spend_script_executed =
list.any(
tx.inputs,
fn(input) {
input.output.address.payment_credential == spend_script_cred
},
)
spend_script_executed?
}
_ -> False
}
}Critical Security Check
Delegation Control
The publish handler validates delegation certificates, ensuring only the admin can change stake pool delegation and that the delegation is to a valid block-producing pool.
pool_mint.ak
A one-shot minting policy that creates the State Thread NFT. This NFT identifies the canonical pool UTxO and ensures there is only ever one active pool state at a time.
One-Shot Property
The policy ensures the NFT can only be minted once by requiring consumption of a specific genesis UTxO:
validator pool_mint(genesis_txhash: ByteArray, genesis_index: Int) {
mint(_redeemer: Void, policy_id: PolicyId, tx: Transaction) {
// Verify the genesis UTxO is consumed
let genesis_consumed =
list.any(
inputs,
fn(input) {
let ref = input.output_reference
ref.transaction_id == genesis_txhash &&
ref.output_index == genesis_index
},
)
// Verify exactly 1 state NFT is minted
let minted_amount = quantity_of(mint, policy_id, state_nft_name)
and {
genesis_consumed?,
(minted_amount == 1)?,
}
}
}Why One-Shot?
How It Works
The Ticket Lottery is a traditional lottery where participants purchase tickets to enter a draw. Unlike the No-Loss Prize Pool, your ticket purchase is not refundable — the risk is real, but so is the potential reward.
1. Buy Tickets
Users purchase tickets at a fixed price (e.g., 2 ADA per ticket). Each ticket purchase is recorded in the TicketDatum. You can buy multiple tickets in a single transaction to increase your odds.
2. Accumulate Pot
All ticket sales go directly into the prize pot. There is no staking involved — what goes in is what gets paid out (minus the 5.5% protocol fee).
3. Commit & Draw
Once enough tickets are sold, the admin initiates a commit-reveal draw:
- Commit: Admin publishes a hash of (secret + salt)
- Draw: Admin reveals secret and salt to determine winner
4. Winner Takes All
The winner receives 94.5% of the total pot. The remaining 5.5% goes to the protocol. The round then resets for the next game.
No Refunds After Purchase
Smart Contract Architecture
The Ticket Lottery uses two smart contracts: a spending validator that handles ticket purchases and draws, and a minting policy for the State Thread NFT.
┌─────────────────────────────────────────────────────────────────┐ │ Ticket Lottery Architecture │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Users Smart Contracts │ │ ────── ─────────────── │ │ │ │ BuyTickets ──► ticket_spend.ak │ │ CancelRound ◄── (State UTxO + State NFT) │ │ DrawWinner ──► │ │ │ │ │ ticket_mint.ak: One-shot State Thread NFT │ │ │ │ Winner receives: 94.5% of pot │ │ Protocol fee: 5.5% │ │ │ └─────────────────────────────────────────────────────────────────┘
| Contract | Type | Purpose |
|---|---|---|
ticket_spend.ak | Spending Validator | Handles ticket purchases, draws, and cancellations |
ticket_mint.ak | Minting Policy | Mints the Ticket Lottery State Thread NFT |
ticket_spend.ak
The main spending validator for the Ticket Lottery. It controls the ticket pool UTxO and validates all ticket purchases, draw execution, and round cancellation.
Redeemers
| Redeemer | Description | Access |
|---|---|---|
BuyTickets | Purchase tickets for the current round | Any user |
CommitTicketDraw | Start commit-reveal randomness phase | Admin only |
DrawTicketWinner | Execute draw, pay winner, reset round | Admin only |
CancelRound | Refund all buyers and reset round | Admin only (before commit) |
BuyTickets Validation
The validate_buy_tickets function ensures:
- Correct payment for ticket count (N × ticket_price)
- No draws are in progress (draw_commit_hash is empty)
- Tickets are recorded with buyer address and exact paid amount
- Pot is increased by the payment amount
fn validate_buy_tickets(
datum: TicketDatum,
out_datum: TicketDatum,
own_value: Value,
script_output: Output,
ticket_count: Int,
buyer: Address,
tx: Transaction,
) -> Bool {
// Calculate expected payment
let ticket_price = datum.ticket_price
let expected_payment = ticket_price * ticket_count
// Verify payment is in the output
let input_lovelace = ada_of(own_value)
let output_lovelace = ada_of(script_output.value)
let paid = output_lovelace - input_lovelace
and {
paid >= expected_payment?,
list.length(out_datum.tickets) == list.length(datum.tickets) + ticket_count?,
out_datum.total_pot == datum.total_pot + paid?,
}
}CancelRound Safety
The CancelRound redeemer provides an important safety mechanism. The admin can cancel a round and refund all buyers, but only before a draw commit has been published:
fn validate_cancel_round(
datum: TicketDatum,
tx: Transaction,
) -> Bool {
// Can only cancel if no commit is active
expect datum.draw_commit_hash == #""
// All buyers must receive exact refund
list.all(datum.tickets, fn(ticket) {
// Verify refund output exists for each buyer
list.any(tx.outputs, fn(output) {
output.address == ticket.buyer &&
ada_of(output.value) >= ticket.paid_lovelace
})
})
}CancelRound Protection
CommitTicketDraw is executed (draw_commit_hash is set),CancelRound is permanently blocked. The admin must complete the draw or the funds remain locked until draw conditions are met. This prevents the admin from holding funds hostage.DrawWinner Validation
Similar to the No-Loss pool, the Ticket Lottery uses commit-reveal randomness. The winner receives 94.5% of the pot, 5.5% goes to protocol fees, and the round resets for the next game.
ticket_mint.ak
A one-shot minting policy that creates the Ticket Lottery State Thread NFT. This NFT uniquely identifies the canonical ticket pool UTxO.
Similar One-Shot Design
Like pool_mint.ak, this policy requires consumption of a specific genesis UTxO to mint the state NFT. Once minted, the NFT can never be recreated, ensuring permanent identification of the ticket pool.
validator ticket_mint(genesis_txhash: ByteArray, genesis_index: Int) {
mint(_redeemer: Void, policy_id: PolicyId, tx: Transaction) {
// Verify the genesis UTxO is consumed
let genesis_consumed =
list.any(
tx.inputs,
fn(input) {
let ref = input.output_reference
ref.transaction_id == genesis_txhash &&
ref.output_index == genesis_index
},
)
// Verify exactly 1 TICKETLOTTO NFT is minted
let minted_amount = quantity_of(tx.mint, policy_id, ticket_nft_name)
and {
genesis_consumed?,
(minted_amount == 1)?,
}
}
}TicketDatum Structure
pub type TicketDatum {
tickets: List<TicketEntry>, // All ticket purchases
total_pot: Lovelace, // Accumulated prize pool
round: Int, // Current round number
draw_commit_hash: ByteArray, // Active commit hash (or empty)
commit_round: Int, // Round when commit was made
admin_pkh: ByteArray, // Admin public key hash
protocol_fee_address: Address, // Where protocol fees go
ticket_price: Lovelace, // Price per ticket (e.g., 2 ADA)
min_tickets_to_draw: Int, // Minimum tickets before draw
}State Management
The No-Loss Prize Pool uses the State Thread NFT pattern to maintain a single canonical state. The State NFT identifies the current valid UTxO that holds all participant deposits and pool data.
PoolDatum Structure
pub type PoolDatum {
pool_id: ByteArray, // Unique pool identifier
total_deposited: Lovelace, // Total ADA in pool
participants: List<Participant>, // All current participants
current_epoch: Int, // Current Cardano epoch
draw_commit_hash: ByteArray, // Active commit hash (or empty)
commit_epoch: Int, // Epoch when commit was made
admin_pkh: ByteArray, // Admin public key hash
protocol_fee_address: Address, // Where protocol fees go
}Participant Structure
pub type Participant {
address: Address, // Participant's Cardano address
deposit_lovelace: Lovelace, // Amount deposited
ticket_weight: Int, // Calculated ticket weight
}Concurrency Considerations
Because Cardano uses the eUTxO model, only one transaction can consume the pool UTxO at a time. This means deposits and withdrawals must be processed sequentially. The State Thread NFT pattern ensures that all operations target the correct, current state.
Randomness
Winner selection uses a two-phase commit-reveal scheme with a minimum 1-epoch delay to ensure tamper-resistant randomness.
Phase 1: Commit
Admin generates a random secret and salt, computesblake2b_256(secret || salt), and publishes only the hash on-chain during epoch N.
Phase 2: Reveal (Epoch N+1 or later)
After at least 1 full epoch, admin reveals secret and salt. The contract verifies the hash and derives entropy from secret, transaction hash, and salt.
Why The Epoch Delay Matters
The minimum 1-epoch delay prevents the admin from predicting the future transaction hash at commit time. Since the transaction hash depends on block contents that are unknown until the epoch arrives, the admin cannot manipulate the draw outcome.
// Entropy = hash(secret || tx_hash || salt)
let entropy = blake2b_256(
bytearray.concat(bytearray.concat(secret, tx.id), salt)
)
// Winner index = entropy mod total tickets
let winner_index = entropy_as_int % total_ticketsUnpredictability Guarantee
Ticket System
Winning odds are proportional to your deposit size. The more you deposit, the more tickets you hold, and the higher your chance of winning.
Ticket Weight Calculation
Tickets are calculated by dividing your deposit by a fixed ticket price. Your principal remains in the pool and continues earning staking rewards:
// 1 ticket = 10 ADA (10_000_000 lovelace)
const ticket_price: Lovelace = 10_000_000
fn calc_ticket_weight(deposit: Lovelace) -> Int {
deposit / ticket_price
}Weighted Random Selection
The select_winner function iterates through participants, accumulating ticket weights until the entropy index is reached:
fn select_winner(
entropy: ByteArray,
participants: List<Participant>
) -> Participant {
let total_tickets = list.foldl(
participants,
0,
fn(p, acc) { acc + p.ticket_weight }
)
let winner_index = byte_array_to_int(entropy) % total_tickets
// Accumulate tickets until we reach winner_index
}Fairness Guarantee
Fee Structure
The protocol charges a fee only on staking rewards, never on principal. This aligns incentives — the protocol only earns when you win.
Winner Receives
94.5%
of accumulated rewards
Protocol Fee
5.5%
goes to protocol maintenance
No Hidden Fees
- No deposit fees
- No withdrawal fees
- No hidden charges
- Your principal is always returned in full
- Only staking rewards are subject to the fee split
State Management
The Ticket Lottery also uses the State Thread NFT pattern. The Ticket Lottery State NFT identifies the UTxO holding the ticket entries and accumulated pot for the current round.
TicketDatum Structure
pub type TicketDatum {
tickets: List<TicketEntry>, // All ticket purchases
total_pot: Lovelace, // Accumulated prize pool
round: Int, // Current round number
draw_commit_hash: ByteArray, // Active commit hash (or empty)
commit_round: Int, // Round when commit was made
admin_pkh: ByteArray, // Admin public key hash
protocol_fee_address: Address, // Where protocol fees go
ticket_price: Lovelace, // Price per ticket (e.g., 2 ADA)
min_tickets_to_draw: Int, // Minimum tickets before draw
}TicketEntry Structure
pub type TicketEntry {
buyer: Address, // Buyer's Cardano address
paid_lovelace: Lovelace, // Exact amount paid for ticket(s)
}Round-Based System
Unlike the No-Loss Pool which runs continuously, the Ticket Lottery operates in rounds. Each round ends when a winner is drawn, and the state resets (tickets cleared, pot reset to 0, round number incremented) for the next game.
Randomness
The Ticket Lottery uses the same commit-reveal scheme as the No-Loss Pool, but without the epoch delay requirement. This is sufficient because the transaction hash unpredictability provides the security guarantee.
Phase 1: Commit
Admin generates a random secret and salt, computesblake2b_256(secret || salt), and publishes only the hash on-chain.
Phase 2: Reveal
Admin reveals secret and salt. The contract verifies the hash and derives entropy. No minimum delay enforced — tx.id provides entropy.
Why No Epoch Delay Is Needed
Unlike the No-Loss Pool (which uses Cardano epochs as a time reference), the Ticket Lottery operates independently of epochs. The commit-reveal scheme alone provides sufficient randomness because the transaction hash is unpredictable at the time of the commit. Requiring an epoch delay would add complexity without security benefit.
// Entropy = hash(secret || tx_hash || salt)
let entropy = blake2b_256(
bytearray.concat(bytearray.concat(secret, tx.id), salt)
)
// Winner index = entropy mod total tickets
let winner_index = entropy_as_int % total_ticketsUnpredictability Guarantee
Ticket System
You buy tickets directly at a fixed price. Each ticket is an entry in the draw — the more tickets you buy, the higher your chance of winning.
Direct Ticket Purchase
Tickets are purchased at a fixed price per ticket. Multiple tickets can be bought in a single transaction:
// 1 ticket = 2 ADA (2_000_000 lovelace)
const ticket_price: Lovelace = 2_000_000
// Buying 5 tickets costs 10 ADA
let ticket_count = 5
let cost = ticket_price * ticket_count // 10_000_000 lovelaceWeighted Random Selection
The select_winner function iterates through ticket entries, accumulating until the entropy index is reached. Each ticket has equal weight.
fn select_winner(
entropy: ByteArray,
tickets: List<TicketEntry>,
total_tickets: Int,
) -> TicketEntry {
let winner_index = byte_array_to_int(entropy) % total_tickets
// Accumulate tickets until we reach winner_index
// ... selection logic
}Fairness Guarantee
Fee Structure
The protocol retains 5.5% of the prize pot. This aligns incentives — the protocol only earns when there is a winner.
Winner Receives
94.5%
of the total pot
Protocol Fee
5.5%
goes to protocol maintenance
Important Notes
- Tickets purchased are spent (not deposits)
- No refunds after purchase (except via CancelRound before commit)
- Winner receives 94.5% of total pot
- CancelRound refunds 100% of ticket costs if round is cancelled before draw commit
Security Model
Smart Contract Security
- Formal verification through Aiken's type system
- State Thread NFT prevents state manipulation
- Commit-reveal prevents draw manipulation
- Stake/reward coupling prevents unauthorized reward withdrawals (No-Loss)
- CancelRound safety mechanism protects Ticket Lottery buyers
Operational Security
- Admin key controls delegation and draw execution only
- Admin cannot access participant funds
- All operations are transparent and auditable on-chain
Ticket Lottery: CancelRound Protection
The Ticket Lottery includes a critical safety feature: CancelRound. Before a draw commit is made, the admin can cancel the round and refund all ticket buyers. However, once CommitTicketDraw is executed, cancellation is permanently blocked — the admin must complete the draw.
- Buyers can be refunded if a round cannot proceed (before commit)
- Admin cannot hold funds hostage after committing to a draw
- On cancel, every buyer receives their exact paid_lovelace (enforced on-chain)
Known Limitations
- Maximum ~50 participants due to datum size limits
- Sequential processing due to eUTxO model
- First draw rewards may be smaller (2-epoch reward delay in No-Loss)
- ~2 ADA min-UTxO lock not part of any deposit
Limitations
| Limitation | Impact | Mitigation |
|---|---|---|
| Participant cap (~50) | Pool size limited | Merkle tree implementation planned |
| Sequential deposits | Potential retry needed | Automatic retry in UI |
| 2-epoch reward delay (No-Loss) | Smaller first draw | Communicated in UI |
| Min ADA lock | ~2 ADA not withdrawable | Protocol-owned, benefits all |
| Ticket Lottery: No time lock | Admin controls draw timing | CancelRound available before commit |
Glossary
- Epoch
- A time period on Cardano lasting approximately 5 days. Rewards are distributed and stake snapshots taken at epoch boundaries.
- eUTxO
- Extended Unspent Transaction Output model used by Cardano. Each UTxO can be spent only once, enabling deterministic smart contract execution.
- Datum
- Data attached to a UTxO that is passed to the validator script when the UTxO is spent.
- Redeemer
- Data provided when spending a script UTxO, typically indicating the action to perform.
- State Thread NFT
- A unique non-fungible token that identifies the canonical state UTxO of a protocol, ensuring state continuity.
- Commit-Reveal
- A two-phase cryptographic protocol where a hash is published first (commit), then the preimage is revealed later to prove consistency.
- Lovelace
- The smallest unit of ADA, where 1 ADA = 1,000,000 Lovelace.
- Stake Pool
- A network node that validates transactions and produces blocks, earning rewards distributed to delegators.
- No-Loss Lottery
- A lottery where participants never risk their principal. Only earned rewards (like staking rewards) are distributed as prizes.
- Ticket Lottery
- A traditional lottery where participants purchase tickets. The ticket cost is not refunded — winners take the accumulated pot.
- CancelRound
- A safety mechanism in the Ticket Lottery allowing the admin to refund all buyers and reset the round, but only before a draw commit is made.
- Ticket Weight
- In the No-Loss pool, the number of tickets a participant holds proportional to their deposit. Used to calculate winning odds.
Last updated: March 2026
← Back to Home