Real-World Applications
This chapter demonstrates practical applications of Psy language features through real-world smart contract examples. We'll examine actual contract implementations to show how the language concepts work together in production scenarios.
Token Contract - Complete Implementation
Let's examine a complete token contract that demonstrates storage, user interaction patterns, and the "read others, write self" security model.
Contract Structure
#[derive(Storage, StorageRef)]
struct OtherUserInfo {
pub amount_sent: Felt,
pub amount_claimed: Felt,
}
#[contract]
#[derive(Storage, StorageRef)]
struct Contract {
pub balance: Felt,
pub other_user_info: [OtherUserInfo; 16777216],
}
Key Design Decisions:
-
Storage Organization: The contract uses a simple two-level structure
balance- Current user's token balanceother_user_info- Array tracking interactions with other users
-
User Isolation: Each user has their own instance of this contract storage
- User A's
balanceis completely separate from User B'sbalance - Cross-user interactions are tracked in the
other_user_infoarray
- User A's
-
Large Array Size:
16777216slots allow tracking interactions with many users- Each slot stores
amount_sentandamount_claimedfor one other user - Uses user ID as array index for O(1) access
- Each slot stores
Token Minting
impl ContractRef {
pub fn simple_mint(amount: Felt) {
let c = ContractRef::new(ContractMetadata::current());
c.balance.set(c.balance.get() + amount);
}
}
Implementation Analysis:
- Storage Access: Uses
ContractMetadata::current()to access current user's storage - Atomic Operation: Reads current balance, adds amount, writes back
- Simplicity: No authorization checks - any user can mint for themselves
- Security: Cannot mint for other users due to storage isolation
Usage Pattern:
ContractRef::simple_mint(100); // Mint 100 tokens to current user
Token Burning with Validation
pub fn simple_burn(amount: Felt) {
let c = ContractRef::new(ContractMetadata::current());
let current_balance = c.balance.get();
assert(current_balance >= amount, "insufficient balance");
c.balance.set(current_balance - amount);
}
Key Features:
- Validation: Checks sufficient balance before burning
- Error Handling: Uses descriptive assertion message
- Safe Arithmetic: No underflow risk due to validation
- Gas Efficiency: Single read, validate, single write pattern
Security Considerations:
- Only user can burn their own tokens (storage isolation)
- Prevents negative balances through validation
- Clear error messages for debugging
Token Transfer - Cross-User Pattern
pub fn simple_transfer(recipient: Felt, amount: Felt) {
let c = ContractRef::new(ContractMetadata::current());
let current_balance = c.balance.get();
assert(current_balance >= amount, "insufficient balance");
let mut recipient_info = c.other_user_info.index(recipient).get();
assert(recipient_info.amount_sent + amount > recipient_info.amount_sent, "amount sent overflow");
c.other_user_info.index(recipient).set(new OtherUserInfo {
amount_sent: recipient_info.amount_sent + amount,
amount_claimed: recipient_info.amount_claimed
});
c.balance.set(current_balance - amount);
}
Transfer Mechanics:
- Sender Validation: Check sender has sufficient balance
- Overflow Protection: Ensure
amount_sentdoesn't overflow - Record Keeping: Update sender's record of tokens sent to recipient
- Balance Update: Deduct from sender's balance
Critical Insight - "Write Self" Pattern:
- Sender can only update their own storage
- Transfer is recorded in sender's
other_user_info[recipient] - Recipient must actively claim tokens (pull pattern)
Why This Design:
- Security: Prevents unauthorized balance modifications
- User Consent: Recipients choose when to claim tokens
- Audit Trail: Complete history of all transfers in sender's storage
Token Claiming - Cross-User Reading
pub fn simple_claim(sender: Felt) {
let self_user_id = get_user_id();
assert(sender != self_user_id, "you cannot claim from your self");
let c = ContractRef::new(ContractMetadata::current());
let sender_user_contract = ContractRef::new(ContractMetadata::new(get_contract_id(), sender));
let sender_total_sent = sender_user_contract.other_user_info.index(self_user_id).get().amount_sent;
let mut sender_info = c.other_user_info.index(sender).get();
assert(sender_info.amount_claimed < sender_total_sent, "no tokens to claim from this sender");
let claimed = sender_total_sent - sender_info.amount_claimed;
c.other_user_info.index(sender).set(new OtherUserInfo {
amount_sent: sender_info.amount_sent,
amount_claimed: sender_total_sent
});
c.balance.set(c.balance.get() + claimed);
}
Claiming Process:
- Identity Validation: Prevent self-claiming
- Read Others: Access sender's storage to see amount sent
- Check Claimable: Compare sent vs already claimed amounts
- Write Self: Update recipient's tracking and balance
Cross-User Storage Access:
// Read from sender's storage
let sender_user_contract = ContractRef::new(ContractMetadata::new(get_contract_id(), sender));
let sender_total_sent = sender_user_contract.other_user_info.index(self_user_id).get().amount_sent;
Security Properties:
- Read Transparency: Can read any user's storage
- Write Isolation: Can only write to own storage
- Double-Spend Prevention: Tracks
amount_claimedto prevent re-claiming - User Control: Recipients decide when to claim
Complete Usage Example
fn main() {
let c = ContractRef::new(ContractMetadata::current());
// Mint initial tokens
ContractRef::simple_mint(100);
assert_eq(c.balance.get(), 100, "c.balance == 100");
// Transfer to another user
ContractRef::simple_transfer(10, 50); // Send 50 tokens to user 10
assert_eq(c.balance.get(), 50, "c.balance == 50");
// Verify transfer was recorded
assert_eq(c.other_user_info.index(10).get().amount_sent, 50, "recorded 50 tokens sent to user 10");
}
Advanced Patterns
Multi-Token Contract with Modules
mod token_types {
pub const GOLD: Felt = 1;
pub const SILVER: Felt = 2;
pub const BRONZE: Felt = 3;
}
mod validation {
use token_types::*;
pub fn is_valid_token_type(token_type: Felt) -> bool {
token_type == GOLD || token_type == SILVER || token_type == BRONZE
}
pub fn get_exchange_rate(from_type: Felt, to_type: Felt) -> Felt {
if from_type == GOLD && to_type == SILVER {
10 // 1 Gold = 10 Silver
} else if from_type == SILVER && to_type == BRONZE {
5 // 1 Silver = 5 Bronze
} else {
1 // 1:1 for same type
}
}
}
#[derive(Storage, StorageRef)]
struct TokenBalance {
pub gold: Felt,
pub silver: Felt,
pub bronze: Felt,
}
#[contract]
#[derive(Storage, StorageRef)]
struct MultiTokenContract {
pub balances: TokenBalance,
pub exchange_history: [Felt; 1000],
}
impl MultiTokenContractRef {
pub fn mint_token(token_type: Felt, amount: Felt) {
use validation::*;
use token_types::*;
assert(is_valid_token_type(token_type), "invalid token type");
let contract = MultiTokenContractRef::new(ContractMetadata::current());
let mut balances = contract.balances.get();
if token_type == GOLD {
balances.gold = balances.gold + amount;
} else if token_type == SILVER {
balances.silver = balances.silver + amount;
} else if token_type == BRONZE {
balances.bronze = balances.bronze + amount;
}
contract.balances.set(balances);
}
pub fn exchange_tokens(from_type: Felt, to_type: Felt, amount: Felt) {
use validation::*;
assert(is_valid_token_type(from_type) && is_valid_token_type(to_type), "invalid token types");
let rate = get_exchange_rate(from_type, to_type);
let converted_amount = amount * rate;
// Implementation would update balances accordingly
// This demonstrates how modules organize related functionality
}
}
Module Benefits:
- Constants Organization:
token_typesmodule centralizes token definitions - Validation Logic:
validationmodule provides reusable checks - Clean Implementation: Contract logic focuses on business rules, not validation details
NFT-Style Contract with Traits
pub trait Transferable {
pub fn can_transfer(token_id: Felt, from: Felt, to: Felt) -> bool;
pub fn transfer(token_id: Felt, to: Felt);
}
pub trait Metadata {
pub fn get_name(token_id: Felt) -> Hash;
pub fn get_description(token_id: Felt) -> Hash;
}
#[derive(Storage, StorageRef)]
struct TokenInfo {
pub owner: Felt,
pub name_hash: Hash,
pub description_hash: Hash,
pub transferable: bool,
}
#[contract]
#[derive(Storage, StorageRef)]
struct NFTContract {
pub tokens: [TokenInfo; 1000000],
pub next_token_id: Felt,
}
impl Transferable for NFTContractRef {
pub fn can_transfer(token_id: Felt, from: Felt, to: Felt) -> bool {
let contract = NFTContractRef::new(ContractMetadata::current());
let token = contract.tokens.index(token_id).get();
token.owner == from && token.transferable
}
pub fn transfer(token_id: Felt, to: Felt) {
let from = get_user_id();
assert(Self::can_transfer(token_id, from, to), "transfer not allowed");
let contract = NFTContractRef::new(ContractMetadata::current());
let mut token = contract.tokens.index(token_id).get();
token.owner = to;
contract.tokens.index(token_id).set(token);
}
}
impl Metadata for NFTContractRef {
pub fn get_name(token_id: Felt) -> Hash {
let contract = NFTContractRef::new(ContractMetadata::current());
contract.tokens.index(token_id).get().name_hash
}
pub fn get_description(token_id: Felt) -> Hash {
let contract = NFTContractRef::new(ContractMetadata::current());
contract.tokens.index(token_id).get().description_hash
}
}
Trait Benefits:
- Interface Separation:
TransferableandMetadatadefine clear contracts - Implementation Flexibility: Different NFT contracts can implement these traits differently
- Code Reusability: Other contracts can implement the same traits
Governance Contract with Generics
pub trait Votable {
pub fn get_voting_power(user_id: Felt) -> Felt;
}
struct ProposalData<T> {
pub id: Felt,
pub description_hash: Hash,
pub votes_for: Felt,
pub votes_against: Felt,
pub metadata: T,
}
#[contract]
#[derive(Storage, StorageRef)]
struct GovernanceContract {
pub proposals: [ProposalData<Hash>; 10000],
pub user_votes: [Felt; 1000000], // Track user voting history
}
impl GovernanceContractRef {
pub fn create_proposal(description_hash: Hash, metadata: Hash) -> Felt {
let contract = GovernanceContractRef::new(ContractMetadata::current());
let proposal_id = get_next_proposal_id();
contract.proposals.index(proposal_id).set(new ProposalData {
id: proposal_id,
description_hash,
votes_for: 0,
votes_against: 0,
metadata
});
proposal_id
}
pub fn vote<V: Votable>(proposal_id: Felt, vote_for: bool) {
let user_id = get_user_id();
let voting_power = V::get_voting_power(user_id);
assert(voting_power > 0, "no voting power");
let contract = GovernanceContractRef::new(ContractMetadata::current());
let mut proposal = contract.proposals.index(proposal_id).get();
if vote_for {
proposal.votes_for = proposal.votes_for + voting_power;
} else {
proposal.votes_against = proposal.votes_against + voting_power;
}
contract.proposals.index(proposal_id).set(proposal);
// Record user's vote to prevent double-voting
contract.user_votes.index(user_id).set(proposal_id);
}
}
fn get_next_proposal_id() -> Felt {
// Implementation would increment and return next available ID
1
}
Generic Benefits:
- Type Flexibility:
ProposalData<T>can hold different metadata types - Constraint Power:
V: Votableensures voting power can be calculated - Extensibility: New voting systems can be plugged in via traits
Design Patterns Summary
1. Storage Patterns
Single-User Data:
pub struct UserData {
pub balance: Felt,
pub last_activity: Felt,
}
Cross-User Tracking:
pub struct UserInteractions {
pub sent: [Felt; MAX_USERS],
pub received: [Felt; MAX_USERS],
}
Hierarchical Data:
pub struct ComplexData {
pub metadata: TokenMetadata,
pub balances: TokenBalances,
pub history: [Transaction; 1000],
}
2. Access Patterns
Current User Access:
let contract = ContractRef::new(ContractMetadata::current());
let value = contract.field.get();
Cross-User Reading:
let other_contract = ContractRef::new(ContractMetadata::new(contract_id, other_user_id));
let other_value = other_contract.field.get();
Safe Writing:
// Only writes to current user's storage
let my_contract = ContractRef::new(ContractMetadata::current());
my_contract.field.set(new_value);
3. Security Patterns
Input Validation:
assert(amount > 0, "amount must be positive");
assert(user_id != get_user_id(), "cannot target self");
Overflow Protection:
assert(balance + amount > balance, "addition overflow");
assert(total_supply + minted > total_supply, "supply overflow");
State Consistency:
let old_state = contract.state.get();
let new_state = update_state(old_state);
contract.state.set(new_state);
These patterns demonstrate how Psy's language features combine to create secure, efficient smart contracts that take advantage of zero-knowledge proofs while maintaining clear, auditable code.