Saltar al contenido principal

Best Practices For Production Contracts

Our guessing game works, but it's not quite ready for production. Let's add professional-grade error handling, security measures, and some clever techniques to make the stored number less obvious to observers.

What We'll Accomplish​

By the end of this step, you'll have:

  • Custom error types instead of panics
  • Robust error handling throughout the contract
  • Better security against common attacks
  • Obfuscated number storage
  • Event logging for monitoring
  • Production-ready smart contract code

Understanding Current Problems​

Our current contract has several issues:

  1. Panic-prone: Uses unwrap() and expect() which crash the contract
  2. Visible number: The secret number is stored in plain sight on-chain
  3. Poor error reporting: Generic error messages don't help users
  4. No events: Hard to monitor what's happening
  5. Basic validation: Doesn't check for edge cases

Let's fix these systematically!

Step 1: Define Custom Error Types​

First, let's create proper error types. Add this after your imports:

#![no_std]
use admin_sep::{Administratable, Upgradable};
use soroban_sdk::{
contract, contractimpl, contracttype, symbol_short, token,
Address, Env, Symbol
};

#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[contracttype]
pub enum Error {
/// Number has not been set yet
NoNumberSet = 1,
/// Guess must be between 1 and 10
InvalidGuess = 2,
/// Insufficient balance to pay guess fee
InsufficientBalance = 3,
/// Prize pot is empty
EmptyPot = 4,
/// Token transfer failed
TransferFailed = 5,
}

Understanding Custom Errors​

Custom errors provide several benefits:

  • Better UX: Users get meaningful error messages
  • No crashes: Contract returns errors instead of panicking
  • Debuggability: Developers can handle different error cases
  • Professional: Shows attention to detail and robustness

Step 2: Add Events for Monitoring​

Let's add events so we can monitor what's happening in our contract:

#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[contracttype]
pub enum Error {
// ... error definitions
}

#[contracttype]
pub struct GameResetEvent {
pub admin: Address,
pub new_pot: u64,
pub funding_amount: u64,
}

#[contracttype]
pub struct GuessEvent {
pub guesser: Address,
pub guess: u64,
pub is_correct: bool,
pub pot_before: u64,
pub winnings: u64,
}

Events help with:

  • Monitoring: Track game activity
  • Analytics: Understand player behavior
  • Debugging: See what happened in past transactions
  • Frontend updates: Real-time UI updates

Step 3: Add Number Obfuscation​

🏗️✨ TODO: deploy to testnet and use contract explorer to see number in plain text

Let's make the stored number less obvious by combining it with a salt. We'll have to store it alongside the number:

const THE_NUMBER: Symbol = symbol_short!("n");
const NUMBER_SALT: Symbol = symbol_short!("salt"); // Add this line

Now let's add functions to handle obfuscated storage:

/// Private helper function to generate and store a new random number with obfuscation
fn set_random_number(env: &Env) {
let new_number: u64 = env.prng().gen_range(1..=10);
let salt: u64 = env.prng().gen();

// Store the number XORed with the salt
let obfuscated = new_number ^ salt;
env.storage().instance().set(&THE_NUMBER, &obfuscated);
env.storage().instance().set(&NUMBER_SALT, &salt);

// etc...
}

/// Private helper to retrieve the actual number
fn get_actual_number(env: &Env) -> Result<u64, Error> {
let obfuscated = env.storage()
.instance()
.get::<_, u64>(&THE_NUMBER)
.ok_or(Error::NoNumberSet)?;

let salt = env.storage()
.instance()
.get::<_, u64>(&NUMBER_SALT)
.ok_or(Error::NoNumberSet)?;

Ok(obfuscated ^ salt)
}

XOR (exclusive or) is a simple but effective obfuscation technique:

  • number ^ salt = obfuscated_value
  • obfuscated_value ^ salt = number (XOR is reversible) Without knowing the salt, the stored value looks random. Note this doesn't provide cryptographic security, but makes casual observation much harder.

Advanced techniques​

🏗️✨ TODO: add explanations

  • commit-reveal pattern
  • off-chain oracle
  • time-delayed reveal

Step 4: Update Functions with Error Handling​

🏗️✨ TODO: clean up code and test

Let's update our main functions to use proper error handling:

/// Update the number and fund the prize pot. Only callable by admin.
pub fn reset(env: &Env, admin_funding: u64) -> Result<(), Error> {
Self::require_admin(env);

// Validate funding amount
if admin_funding == 0 {
return Err(Error::EmptyPot);
}

// Generate new number
Self::set_random_number(env);

// Get admin address
let admin = Self::get_admin(env);

// Get current pot before adding funding
let current_pot = Self::get_prize_pot(env);

// Transfer funding from admin to contract
let native_token = Self::get_native_token(env);
let token_client = token::Client::new(env, &native_token);

// This could fail, so we need to handle it
token_client.transfer(&admin, &env.current_contract_address(), &(admin_funding as i128));

// Add to prize pot
let new_pot = current_pot + admin_funding;
env.storage().instance().set(&PRIZE_POT, &new_pot);

// Emit event
env.events().publish(
(symbol_short!("reset"),),
GameResetEvent {
admin: admin.clone(),
new_pot,
funding_amount: admin_funding,
},
);

Ok(())
}

Now the improved guess function:

/// Guess a number between 1 and 10
/// Costs a fee and pays out the entire pot if correct
pub fn guess(env: &Env, guesser: Address, a_number: u64) -> Result<bool, Error> {
// Verify the guesser is actually the one calling this function
guesser.require_auth();

// Validate guess is in range
if a_number < 1 || a_number > 10 {
return Err(Error::InvalidGuess);
}

// ...

// Emit event
env.events().publish(
(symbol_short!("guess"),),
GuessEvent {
guesser: guesser.clone(),
guess: a_number,
is_correct,
pot_before: prize_pot,
winnings,
},
);

Ok(is_correct)
}

Step 5: Admin Functions?​

🏗️✨ TODO: is this necessary?

Here's your complete, production-ready contract:

🏗️✨ TODO: add link to github repo of sample project

Step 7: Test the Secure Contract​

Let's test all our new security and error handling features:

Test Error Handling​

Let's test that our error handling works:

# Test invalid guess (should return error, not crash)
stellar contract invoke \
--id [CONTRACT_ID] \
--source bob \
--network local \
-- guess \
--guesser $(stellar keys address bob) \
--a_number 15

You should get a proper error message instead of a crash!

Test Contract Info​

stellar contract invoke \
--id [CONTRACT_ID] \
--source alice \
--network local \
-- get_contract_info

This should return [0, 100000, true] showing: pot=0, fee=100k stroops, number is set.

Test the Event System​

When you make guesses and resets, you should see events in the transaction response. These events can be monitored by frontend applications for real-time updates.

Test Emergency Functions​

# First fund the contract
stellar contract invoke \
--id [CONTRACT_ID] \
--source alice \
--network local \
-- reset --admin_funding 5000000

# Test emergency withdraw (admin only)
stellar contract invoke \
--id [CONTRACT_ID] \
--source alice \
--network local \
-- emergency_withdraw

Step 8: Gas Optimization?​

🏗️✨ TODO

Our current contract is quite efficient, but for even better performance:

  • Use Temporary storage for short-lived data
  • Batch operations when possible
  • Avoid unnecessary storage reads

What We've Accomplished​

Our contract has evolved from a simple demo to production-ready code:

Security Improvements​

  • ✅ Custom error types: No more crashes, proper error handling
  • ✅ Input validation: Check all user inputs
  • ✅ Obfuscated storage: Secret number is not obvious
  • ✅ Emergency functions: Admin can recover funds if needed
  • ✅ Event logging: Full audit trail of all actions

Code Quality​

  • ✅ Proper error propagation: Using Result<T, Error> throughout
  • ✅ Defensive programming: Check all assumptions
  • ✅ Clean separation: Private helpers for internal logic
  • ✅ Documentation: Clear function purposes and error cases

User Experience​

  • ✅ Meaningful errors: Users know exactly what went wrong
  • ✅ Real-time events: Frontend can show live updates
  • ✅ Admin tools: Easy management and monitoring
  • ✅ Reliability: Contract won't crash under normal or edge cases

Production Deployment Checklist​

Before deploying to mainnet, ensure:

  1. Testing: Comprehensive test suite covering all edge cases
  2. Security audit: Have the contract reviewed by security experts
  3. Gas optimization: Minimize transaction costs
  4. Monitoring: Set up event monitoring and alerting
  5. Admin procedures: Document emergency procedures
  6. User documentation: Clear instructions for users

Comparing Our Journey​

Let's look at how far we've come:

Step 1: Basic Contract​

  • Simple number storage
  • Panic-prone code
  • No authentication

Step 2: Development Workflow and a UI​

  • Constructor initialization
  • Private helper functions
  • Basic error handling

Step 3: Adding Transactions​

  • Proper authorization
  • Token transfers
  • Financial incentives

Step 4: Production Ready​

  • Comprehensive error handling
  • Security measures
  • Professional monitoring

Congratulations!​

You've built a complete, production-ready smart contract that demonstrates:

  • Proper Rust and Soroban patterns
  • Economic incentives and token handling
  • Professional error handling and security
  • Real-world blockchain development practices

This knowledge forms the foundation for building more complex decentralized applications. The patterns you've learned - authentication, token transfers, error handling, and event emission - appear in virtually every serious blockchain application.

Keep building, keep learning, and welcome to the world of decentralized application development!