diff --git a/Cargo.toml b/Cargo.toml index 34fcab1d..aca7af0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ + "shared", "examples/basics/*", "examples/intermediate/*", "examples/advanced/*", diff --git a/docs/common-patterns.md b/docs/common-patterns.md index 0b2264f1..89301655 100644 --- a/docs/common-patterns.md +++ b/docs/common-patterns.md @@ -45,9 +45,34 @@ pub fn transfer(env: Env, from: Address, to: Address, amount: i128) return Err(AuthError::InsufficientBalance); } - let bal: i128 = env.storage().persistent() - .get(&DataKey::Balance(from.clone())).unwrap_or(0); - if bal < amount { +--- + +## 3. Shared Validation Helpers + +**Source:** [`shared/`](../shared/src/lib.rs), [`06-validation-patterns`](../examples/basics/06-validation-patterns/src/lib.rs) + +Use shared validation utilities for common validation patterns. These provide +consistent error handling and reduce code duplication across contracts. + +```rust +use soroban_validation::*; + +// Parameter validation +validate_amount(amount, 1, 1000000)?; +validate_address(address)?; + +// State validation +require_sufficient_balance(balance, required_amount)?; +require_cooldown_expired(&env, last_action, 3600)?; + +// Authorization validation +require_owner(stored_owner, caller)?; +require_admin(stored_admin, caller)?; +``` + +**When to use:** For any input validation, state checking, or authorization logic. +Import `soroban_validation::*` and use the appropriate validation functions. +Always handle validation errors appropriately in your contract logic. return Err(AuthError::InsufficientBalance); } diff --git a/examples/basics/03-authentication/Cargo.toml b/examples/basics/03-authentication/Cargo.toml index be7c692d..84fbf4ed 100644 --- a/examples/basics/03-authentication/Cargo.toml +++ b/examples/basics/03-authentication/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = { workspace = true } +soroban-validation = { path = "../../../shared" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/examples/basics/03-authentication/src/lib.rs b/examples/basics/03-authentication/src/lib.rs index c5228556..a5af4a89 100644 --- a/examples/basics/03-authentication/src/lib.rs +++ b/examples/basics/03-authentication/src/lib.rs @@ -24,6 +24,7 @@ use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, vec, Address, Env, Symbol, Vec, }; +use soroban_validation::*; // --------------------------------------------------------------------------- // Role definitions @@ -192,7 +193,8 @@ impl AuthContract { .get(&DataKey::Admin) .ok_or(AuthError::NotAdmin)?; - if admin != stored_admin { + // Use shared validation pattern + if require_admin(stored_admin, admin.clone()).is_err() { return Err(AuthError::NotAdmin); } @@ -222,7 +224,8 @@ impl AuthContract { .get(&DataKey::Admin) .ok_or(AuthError::NotAdmin)?; - if admin != stored_admin { + // Use shared validation pattern + if require_admin(stored_admin, admin.clone()).is_err() { return Err(AuthError::NotAdmin); } @@ -261,7 +264,8 @@ impl AuthContract { .get(&DataKey::Balance(from.clone())) .unwrap_or(0); - if amount <= 0 || from_balance < amount { + // Use shared validation pattern + if require_sufficient_balance(from_balance, amount).is_err() { return Err(AuthError::InsufficientBalance); } diff --git a/examples/basics/06-validation-patterns/Cargo.toml b/examples/basics/06-validation-patterns/Cargo.toml index ad83fc44..21ec1135 100644 --- a/examples/basics/06-validation-patterns/Cargo.toml +++ b/examples/basics/06-validation-patterns/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = { workspace = true } +soroban-validation = { path = "../../../shared" } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/examples/basics/06-validation-patterns/README.md b/examples/basics/06-validation-patterns/README.md index 2ab5cae6..285f78e1 100644 --- a/examples/basics/06-validation-patterns/README.md +++ b/examples/basics/06-validation-patterns/README.md @@ -84,48 +84,45 @@ ValidationError::Blacklisted = 309, ```rust // Validate amount with min/max bounds -validate_amount_parameters(amount, min_amount, max_amount) +soroban_validation::validate_amount(amount, min_amount, max_amount) // Validate string length and content -validate_string_parameters(text, min_length, max_length) +soroban_validation::validate_string(text, min_length, max_length) // Validate address format -validate_address(address) +soroban_validation::validate_address(address) // Validate array size -validate_array_parameters(array, min_size, max_size) +soroban_validation::validate_array(array, min_size, max_size) // Validate timestamp range -validate_timestamp_parameters(env, timestamp, allow_past, max_future_seconds) +soroban_validation::validate_timestamp(env, timestamp, allow_past, max_future_seconds) ``` ### State Validation Functions ```rust -// Validate contract is in required state -validate_contract_state(env, required_state) - // Validate sufficient balance -validate_balance(env, address, required_amount) - -// Validate sufficient allowance -validate_allowance(env, owner, spender, required_amount) +soroban_validation::require_sufficient_balance(current_balance, required_amount) // Validate cooldown period -validate_cooldown(env, address, cooldown_seconds) +soroban_validation::require_cooldown_expired(env, last_action_time, cooldown_seconds) ``` ### Authorization Validation Functions ```rust -// Validate user has sufficient role -validate_role(env, address, required_role) - // Validate ownership -validate_ownership(env, address) +soroban_validation::require_owner(stored_owner, claimed_owner) // Validate admin permissions -validate_admin(env, address) +soroban_validation::require_admin(stored_admin, claimed_admin) + +// Validate role hierarchy +soroban_validation::require_role(user_role, required_role) + +// Validate blacklist status +soroban_validation::require_not_blacklisted(is_blacklisted) ``` ## Usage Examples @@ -133,6 +130,8 @@ validate_admin(env, address) ### Basic Transfer with Full Validation ```rust +use soroban_validation::*; + let result = client.validated_transfer( &from_address, &to_address, @@ -148,6 +147,40 @@ match result { } ``` +### Using Shared Validators in Contract Functions + +```rust +use soroban_sdk::*; +use soroban_validation::*; + +#[contractimpl] +impl MyContract { + pub fn transfer_with_validation( + env: Env, + from: Address, + to: Address, + amount: i128 + ) -> Result<(), ValidationError> { + from.require_auth(); + + // Parameter validation + validate_amount(amount, 1, 1000000)?; + validate_address(from.clone())?; + validate_address(to.clone())?; + + // State validation + let balance = get_balance(&env, from.clone()); + require_sufficient_balance(balance, amount)?; + + // Authorization validation (if needed) + // require_owner(stored_owner, from.clone())?; + + // Execute transfer logic... + Ok(()) + } +} +``` + ### Admin Operations with Authorization ```rust diff --git a/examples/basics/06-validation-patterns/src/lib.rs b/examples/basics/06-validation-patterns/src/lib.rs index 8591cb9c..139ffd27 100644 --- a/examples/basics/06-validation-patterns/src/lib.rs +++ b/examples/basics/06-validation-patterns/src/lib.rs @@ -24,57 +24,8 @@ //! - Permission checks for specific operations #![no_std] -use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, String, Vec}; - -// --------------------------------------------------------------------------- -// Error Types -// --------------------------------------------------------------------------- - -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[repr(u32)] -pub enum ValidationError { - // Parameter validation errors (100-199) - InvalidAmount = 100, - AmountTooSmall = 101, - AmountTooLarge = 102, - InvalidAddress = 103, - InvalidString = 104, - StringTooShort = 105, - StringTooLong = 106, - InvalidEnum = 107, - InvalidArray = 108, - ArrayTooSmall = 109, - ArrayTooLarge = 110, - InvalidTimestamp = 111, - TimestampInPast = 112, - TimestampInDistantFuture = 113, - - // State validation errors (200-299) - ContractNotInitialized = 200, - ContractPaused = 201, - ContractFrozen = 202, - InsufficientBalance = 203, - InsufficientAllowance = 204, - ResourceNotFound = 205, - ResourceAlreadyExists = 206, - InvalidStateTransition = 207, - InvariantViolation = 208, - RateLimitExceeded = 209, - CooldownActive = 210, - - // Authorization validation errors (300-399) - Unauthorized = 300, - NotAdmin = 301, - NotOwner = 302, - InsufficientRole = 303, - SignatureRequired = 304, - MultiSigRequired = 305, - InvalidSignature = 306, - ExpiredSignature = 307, - WrongContract = 308, - Blacklisted = 309, -} +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String, Vec}; +use soroban_validation::*; // --------------------------------------------------------------------------- // Data Types @@ -177,21 +128,8 @@ impl ValidationContract { min_amount: i128, max_amount: i128, ) -> Result<(), ValidationError> { - // Basic amount validation - if amount <= 0 { - return Err(ValidationError::InvalidAmount); - } - - // Range validation - if amount < min_amount { - return Err(ValidationError::AmountTooSmall); - } - - if amount > max_amount { - return Err(ValidationError::AmountTooLarge); - } - - Ok(()) + // Use shared validation function + validate_amount(amount, min_amount, max_amount) } /// Example of string parameter validation @@ -210,23 +148,8 @@ impl ValidationContract { min_length: u32, max_length: u32, ) -> Result<(), ValidationError> { - let length = text.len(); - - // Length validation - if length < min_length { - return Err(ValidationError::StringTooShort); - } - - if length > max_length { - return Err(ValidationError::StringTooLong); - } - - // Content validation (example: no empty strings) - if length == 0 { - return Err(ValidationError::InvalidString); - } - - Ok(()) + // Use shared validation function + validate_string(text, min_length, max_length) } /// Example of address parameter validation @@ -236,11 +159,9 @@ impl ValidationContract { /// /// # Errors /// * `ValidationError::InvalidAddress` - If address is invalid - pub fn validate_address(_address: Address) -> Result<(), ValidationError> { - // In Soroban, addresses are always valid if they exist - // This is a placeholder for more complex address validation - // such as checking against a blacklist or whitelist - Ok(()) + pub fn validate_address(address: Address) -> Result<(), ValidationError> { + // Use shared validation function + validate_address(address) } /// Example of array parameter validation @@ -258,17 +179,8 @@ impl ValidationContract { min_size: u32, max_size: u32, ) -> Result<(), ValidationError> { - let size = array.len(); - - if size < min_size { - return Err(ValidationError::ArrayTooSmall); - } - - if size > max_size { - return Err(ValidationError::ArrayTooLarge); - } - - Ok(()) + // Use shared validation function + validate_array(array, min_size, max_size) } /// Example of timestamp parameter validation @@ -288,19 +200,8 @@ impl ValidationContract { allow_past: bool, max_future_seconds: u64, ) -> Result<(), ValidationError> { - let current_time = env.ledger().timestamp(); - - // Check if timestamp is in the past (when not allowed) - if !allow_past && timestamp < current_time { - return Err(ValidationError::TimestampInPast); - } - - // Check if timestamp is too far in the future - if timestamp > current_time + max_future_seconds { - return Err(ValidationError::TimestampInDistantFuture); - } - - Ok(()) + // Use shared validation function + validate_timestamp(env, timestamp, allow_past, max_future_seconds) } // ==================== STATE VALIDATION EXAMPLES ==================== @@ -367,11 +268,8 @@ impl ValidationContract { .get(&DataKey::Balance(address.clone())) .unwrap_or(0); - if balance < required_amount { - return Err(ValidationError::InsufficientBalance); - } - - Ok(()) + // Use shared validation pattern + require_sufficient_balance(balance, required_amount) } /// Example of allowance validation @@ -422,14 +320,11 @@ impl ValidationContract { .persistent() .get::(&DataKey::LastAction(address.clone())) { - let current_time = env.ledger().timestamp(); - - if current_time < last_action + cooldown_seconds { - return Err(ValidationError::CooldownActive); - } + // Use shared validation pattern + require_cooldown_expired(env, last_action, cooldown_seconds) + } else { + Ok(()) } - - Ok(()) } // ==================== AUTHORIZATION VALIDATION EXAMPLES ==================== @@ -452,13 +347,11 @@ impl ValidationContract { required_role: UserRole, ) -> Result<(), ValidationError> { // Check if address is blacklisted - if env + let is_blacklisted = env .storage() .instance() - .has(&DataKey::Blacklist(address.clone())) - { - return Err(ValidationError::Blacklisted); - } + .has(&DataKey::Blacklist(address.clone())); + require_not_blacklisted(is_blacklisted)?; // Get user role let user_role: UserRole = env @@ -467,10 +360,8 @@ impl ValidationContract { .get(&DataKey::UserRole(address.clone())) .unwrap_or(UserRole::None); - // Check role hierarchy - if user_role < required_role { - return Err(ValidationError::InsufficientRole); - } + // Use shared validation pattern for role comparison + require_role(user_role, required_role)?; // Special checks for owner and admin match required_role { @@ -501,11 +392,8 @@ impl ValidationContract { .get(&DataKey::Owner) .ok_or(ValidationError::ContractNotInitialized)?; - if address != owner { - return Err(ValidationError::NotOwner); - } - - Ok(()) + // Use shared validation pattern + require_owner(owner, address) } /// Example of admin validation @@ -523,11 +411,8 @@ impl ValidationContract { .get(&DataKey::Admin) .ok_or(ValidationError::ContractNotInitialized)?; - if address != admin { - return Err(ValidationError::NotAdmin); - } - - Ok(()) + // Use shared validation pattern + require_admin(admin, address) } // ==================== COMBINED VALIDATION EXAMPLES ==================== diff --git a/shared/Cargo.toml b/shared/Cargo.toml new file mode 100644 index 00000000..8de98b74 --- /dev/null +++ b/shared/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "soroban-validation" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["rlib"] + +[dependencies] +soroban-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } \ No newline at end of file diff --git a/shared/README.md b/shared/README.md new file mode 100644 index 00000000..0bf6ec4e --- /dev/null +++ b/shared/README.md @@ -0,0 +1,242 @@ +# Soroban Validation Library + +A collection of reusable validation utilities for Soroban smart contracts. This library provides typed validation errors and helpers for parameter validation, state validation, and authorization validation. + +## Overview + +This library extracts common validation patterns used across Soroban contracts into reusable functions. It provides: + +- **Parameter Validation**: Validate inputs like amounts, addresses, strings, arrays, and timestamps +- **State Validation**: Validate contract state, balances, allowances, and temporal constraints +- **Authorization Validation**: Validate permissions, roles, ownership, and access controls +- **Typed Errors**: Comprehensive error types for clear error handling + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +soroban-validation = { path = "../../../shared" } +``` + +## Error Types + +All validation functions return `Result<(), ValidationError>` with the following error codes: + +### Parameter Validation Errors (100-199) +- `InvalidAmount` (100): Amount is negative or zero +- `AmountTooSmall` (101): Amount below minimum +- `AmountTooLarge` (102): Amount exceeds maximum +- `InvalidAddress` (103): Invalid address format +- `InvalidString` (104): Invalid string content +- `StringTooShort` (105): String too short +- `StringTooLong` (106): String too long +- `InvalidArray` (108): Invalid array content +- `ArrayTooSmall` (109): Array too small +- `ArrayTooLarge` (110): Array too large +- `InvalidTimestamp` (111): Invalid timestamp +- `TimestampInPast` (112): Timestamp in past when not allowed +- `TimestampInDistantFuture` (113): Timestamp too far in future + +### State Validation Errors (200-299) +- `ContractNotInitialized` (200): Contract not initialized +- `ContractPaused` (201): Contract is paused +- `ContractFrozen` (202): Contract is frozen +- `InsufficientBalance` (203): Insufficient balance +- `InsufficientAllowance` (204): Insufficient allowance +- `ResourceNotFound` (205): Resource not found +- `ResourceAlreadyExists` (206): Resource already exists +- `InvalidStateTransition` (207): Invalid state transition +- `CooldownActive` (210): Cooldown period active + +### Authorization Validation Errors (300-399) +- `Unauthorized` (300): Unauthorized access +- `NotAdmin` (301): Not an admin +- `NotOwner` (302): Not the owner +- `InsufficientRole` (303): Insufficient role permissions +- `Blacklisted` (309): Address is blacklisted + +## Parameter Validation + +### Amount Validation + +```rust +use soroban_validation::validate_amount; + +let amount = 100i128; +let min_amount = 1i128; +let max_amount = 1000000i128; + +validate_amount(amount, min_amount, max_amount)?; +``` + +### String Validation + +```rust +use soroban_validation::validate_string; + +let text = String::from_str(&env, "hello"); +validate_string(text, 1, 100)?; // min_length=1, max_length=100 +``` + +### Address Validation + +```rust +use soroban_validation::validate_address; + +let address = Address::generate(&env); +validate_address(address)?; +``` + +### Array Validation + +```rust +use soroban_validation::validate_array; + +let array = Vec::from_array(&env, [1, 2, 3]); +validate_array(array, 1, 10)?; // min_size=1, max_size=10 +``` + +### Timestamp Validation + +```rust +use soroban_validation::validate_timestamp; + +let timestamp = 1234567890u64; +let allow_past = false; +let max_future_seconds = 86400; // 1 day + +validate_timestamp(&env, timestamp, allow_past, max_future_seconds)?; +``` + +## State Validation + +### Balance Validation + +```rust +use soroban_validation::require_sufficient_balance; + +let current_balance = 1000i128; +let required_amount = 100i128; + +require_sufficient_balance(current_balance, required_amount)?; +``` + +### Cooldown Validation + +```rust +use soroban_validation::require_cooldown_expired; + +let last_action_time = 1234567800u64; +let cooldown_seconds = 3600; // 1 hour + +require_cooldown_expired(&env, last_action_time, cooldown_seconds)?; +``` + +## Authorization Validation + +### Ownership Validation + +```rust +use soroban_validation::require_owner; + +let stored_owner = Address::generate(&env); +let claimed_owner = Address::generate(&env); + +require_owner(stored_owner, claimed_owner)?; +``` + +### Admin Validation + +```rust +use soroban_validation::require_admin; + +let stored_admin = Address::generate(&env); +let claimed_admin = Address::generate(&env); + +require_admin(stored_admin, claimed_admin)?; +``` + +### Role Validation + +```rust +use soroban_validation::require_role; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +enum UserRole { User, Moderator, Admin } + +let user_role = UserRole::Moderator; +let required_role = UserRole::Admin; + +require_role(user_role, required_role)?; +``` + +### Blacklist Validation + +```rust +use soroban_validation::require_not_blacklisted; + +let is_blacklisted = false; +require_not_blacklisted(is_blacklisted)?; +``` + +## Complete Example + +```rust +use soroban_sdk::*; +use soroban_validation::*; + +#[contract] +pub struct TokenContract; + +#[contractimpl] +impl TokenContract { + pub fn transfer( + env: Env, + from: Address, + to: Address, + amount: i128 + ) -> Result<(), ValidationError> { + from.require_auth(); + + // Parameter validation + validate_amount(amount, 1, i128::MAX)?; + validate_address(from.clone())?; + validate_address(to.clone())?; + + // State validation + let balance = get_balance(&env, from.clone()); + require_sufficient_balance(balance, amount)?; + + // Check cooldown (example) + let last_transfer = get_last_transfer(&env, from.clone()); + require_cooldown_expired(&env, last_transfer, 60)?; // 1 minute cooldown + + // Execute transfer + update_balance(&env, from, balance - amount); + update_balance(&env, to, get_balance(&env, to) + amount); + update_last_transfer(&env, from, env.ledger().timestamp()); + + Ok(()) + } +} +``` + +## Best Practices + +1. **Validate Early**: Call validation functions at the beginning of your contract functions +2. **Use Appropriate Errors**: Choose error types that clearly indicate what went wrong +3. **Combine Validations**: Use multiple validation functions for comprehensive input checking +4. **Handle Errors Gracefully**: Provide clear error messages to users +5. **Test Thoroughly**: Test both success and failure cases for all validation scenarios + +## Contributing + +When adding new validation functions: + +1. Add appropriate error codes in the `ValidationError` enum +2. Follow the existing naming conventions +3. Include comprehensive documentation +4. Add unit tests +5. Update this README \ No newline at end of file diff --git a/shared/src/lib.rs b/shared/src/lib.rs new file mode 100644 index 00000000..19057d93 --- /dev/null +++ b/shared/src/lib.rs @@ -0,0 +1,406 @@ +//! # Soroban Validation Library +//! +//! A collection of reusable validation utilities for Soroban smart contracts. +//! Provides typed validation errors and helpers for parameter, state, and authorization validation. +//! +//! ## Categories of Validation +//! +//! ### Parameter Validation +//! Validates function inputs such as amounts, addresses, strings, arrays, and timestamps. +//! +//! ### State Validation +//! Provides patterns and utilities for validating contract state, balances, and other stored data. +//! +//! ### Authorization Validation +//! Provides patterns and utilities for validating user permissions and access controls. + +#![no_std] +use soroban_sdk::{Address, Env, String, Vec, contracterror}; + +// --------------------------------------------------------------------------- +// Error Types +// --------------------------------------------------------------------------- + +/// Comprehensive validation error types for Soroban contracts +#[contracterror] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum ValidationError { + // Parameter validation errors (100-199) + InvalidAmount = 100, + AmountTooSmall = 101, + AmountTooLarge = 102, + InvalidAddress = 103, + InvalidString = 104, + StringTooShort = 105, + StringTooLong = 106, + InvalidEnum = 107, + InvalidArray = 108, + ArrayTooSmall = 109, + ArrayTooLarge = 110, + InvalidTimestamp = 111, + TimestampInPast = 112, + TimestampInDistantFuture = 113, + + // State validation errors (200-299) + ContractNotInitialized = 200, + ContractPaused = 201, + ContractFrozen = 202, + InsufficientBalance = 203, + InsufficientAllowance = 204, + ResourceNotFound = 205, + ResourceAlreadyExists = 206, + InvalidStateTransition = 207, + InvariantViolation = 208, + RateLimitExceeded = 209, + CooldownActive = 210, + + // Authorization validation errors (300-399) + Unauthorized = 300, + NotAdmin = 301, + NotOwner = 302, + InsufficientRole = 303, + SignatureRequired = 304, + MultiSigRequired = 305, + InvalidSignature = 306, + ExpiredSignature = 307, + WrongContract = 308, + Blacklisted = 309, +} + +// --------------------------------------------------------------------------- +// Parameter Validation Functions +// --------------------------------------------------------------------------- + +/// Validates amount parameters with min/max bounds +/// +/// # Arguments +/// * `amount` - The amount to validate +/// * `min_amount` - Minimum allowed amount (inclusive) +/// * `max_amount` - Maximum allowed amount (inclusive) +/// +/// # Errors +/// * `ValidationError::InvalidAmount` - If amount is negative or zero +/// * `ValidationError::AmountTooSmall` - If amount is below minimum +/// * `ValidationError::AmountTooLarge` - If amount exceeds maximum +pub fn validate_amount(amount: i128, min_amount: i128, max_amount: i128) -> Result<(), ValidationError> { + // Basic amount validation + if amount <= 0 { + return Err(ValidationError::InvalidAmount); + } + + // Range validation + if amount < min_amount { + return Err(ValidationError::AmountTooSmall); + } + + if amount > max_amount { + return Err(ValidationError::AmountTooLarge); + } + + Ok(()) +} + +/// Validates string parameters with length constraints +/// +/// # Arguments +/// * `text` - The string to validate +/// * `min_length` - Minimum required length (inclusive) +/// * `max_length` - Maximum allowed length (inclusive) +/// +/// # Errors +/// * `ValidationError::InvalidString` - If string contains invalid characters or is empty when min_length > 0 +/// * `ValidationError::StringTooShort` - If string is too short +/// * `ValidationError::StringTooLong` - If string is too long +pub fn validate_string(text: String, min_length: u32, max_length: u32) -> Result<(), ValidationError> { + let length = text.len(); + + // Length validation + if length < min_length { + return Err(ValidationError::StringTooShort); + } + + if length > max_length { + return Err(ValidationError::StringTooLong); + } + + // Content validation (example: no empty strings when min_length > 0) + if min_length > 0 && length == 0 { + return Err(ValidationError::InvalidString); + } + + Ok(()) +} + +/// Validates address parameters +/// +/// # Arguments +/// * `_address` - The address to validate +/// +/// # Errors +/// * `ValidationError::InvalidAddress` - If address is invalid +/// +/// # Note +/// In Soroban, addresses are always valid if they exist. +/// This function is a placeholder for more complex address validation +/// such as checking against a blacklist or whitelist. +pub fn validate_address(_address: Address) -> Result<(), ValidationError> { + // In Soroban, addresses are always valid if they exist + // This is a placeholder for more complex address validation + Ok(()) +} + +/// Validates array parameters with size constraints +/// +/// # Arguments +/// * `array` - The array to validate +/// * `min_size` - Minimum required size (inclusive) +/// * `max_size` - Maximum allowed size (inclusive) +/// +/// # Errors +/// * `ValidationError::ArrayTooSmall` - If array is too small +/// * `ValidationError::ArrayTooLarge` - If array is too large +pub fn validate_array(array: Vec, min_size: u32, max_size: u32) -> Result<(), ValidationError> { + let size = array.len(); + + if size < min_size { + return Err(ValidationError::ArrayTooSmall); + } + + if size > max_size { + return Err(ValidationError::ArrayTooLarge); + } + + Ok(()) +} + +/// Validates timestamp parameters with temporal constraints +/// +/// # Arguments +/// * `env` - The contract environment +/// * `timestamp` - The timestamp to validate +/// * `allow_past` - Whether past timestamps are allowed +/// * `max_future_seconds` - Maximum seconds in the future allowed +/// +/// # Errors +/// * `ValidationError::InvalidTimestamp` - If timestamp is invalid +/// * `ValidationError::TimestampInPast` - If timestamp is in the past (when not allowed) +/// * `ValidationError::TimestampInDistantFuture` - If timestamp is too far in the future +pub fn validate_timestamp( + env: &Env, + timestamp: u64, + allow_past: bool, + max_future_seconds: u64, +) -> Result<(), ValidationError> { + let current_time = env.ledger().timestamp(); + + // Check if timestamp is in the past (when not allowed) + if !allow_past && timestamp < current_time { + return Err(ValidationError::TimestampInPast); + } + + // Check if timestamp is too far in the future + if timestamp > current_time + max_future_seconds { + return Err(ValidationError::TimestampInDistantFuture); + } + + Ok(()) +} + +// --------------------------------------------------------------------------- +// State Validation Patterns +// --------------------------------------------------------------------------- + +/// Pattern for validating contract initialization +/// +/// # Arguments +/// * `env` - The contract environment +/// * `key` - The key to check for existence +/// +/// # Errors +/// * `ValidationError::ContractNotInitialized` - If the key doesn't exist +pub fn require_initialized( + env: &Env, + key: &K, +) -> Result<(), ValidationError> +where + K: soroban_sdk::TryFromVal + soroban_sdk::IntoVal, +{ + if !env.storage().instance().has(key) { + return Err(ValidationError::ContractNotInitialized); + } + Ok(()) +} + +/// Pattern for validating sufficient balance +/// +/// # Arguments +/// * `current_balance` - The current balance +/// * `required_amount` - The required amount +/// +/// # Errors +/// * `ValidationError::InsufficientBalance` - If balance is insufficient +pub fn require_sufficient_balance( + current_balance: i128, + required_amount: i128, +) -> Result<(), ValidationError> { + if current_balance < required_amount { + return Err(ValidationError::InsufficientBalance); + } + Ok(()) +} + +/// Pattern for validating cooldown periods +/// +/// # Arguments +/// * `env` - The contract environment +/// * `last_action_time` - The timestamp of the last action +/// * `cooldown_seconds` - The required cooldown period +/// +/// # Errors +/// * `ValidationError::CooldownActive` - If cooldown is still active +pub fn require_cooldown_expired( + env: &Env, + last_action_time: u64, + cooldown_seconds: u64, +) -> Result<(), ValidationError> { + let current_time = env.ledger().timestamp(); + if current_time < last_action_time + cooldown_seconds { + return Err(ValidationError::CooldownActive); + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// Authorization Validation Patterns +// --------------------------------------------------------------------------- + +/// Pattern for validating ownership +/// +/// # Arguments +/// * `stored_owner` - The stored owner address +/// * `claimed_owner` - The address claiming to be owner +/// +/// # Errors +/// * `ValidationError::NotOwner` - If addresses don't match +pub fn require_owner(stored_owner: Address, claimed_owner: Address) -> Result<(), ValidationError> { + if stored_owner != claimed_owner { + return Err(ValidationError::NotOwner); + } + Ok(()) +} + +/// Pattern for validating admin permissions +/// +/// # Arguments +/// * `stored_admin` - The stored admin address +/// * `claimed_admin` - The address claiming to be admin +/// +/// # Errors +/// * `ValidationError::NotAdmin` - If addresses don't match +pub fn require_admin(stored_admin: Address, claimed_admin: Address) -> Result<(), ValidationError> { + if stored_admin != claimed_admin { + return Err(ValidationError::NotAdmin); + } + Ok(()) +} + +/// Pattern for validating role hierarchy +/// +/// # Arguments +/// * `user_role` - The user's current role +/// * `required_role` - The minimum required role +/// +/// # Type Parameters +/// * `R` - The role type that implements Ord +/// +/// # Errors +/// * `ValidationError::InsufficientRole` - If user role is insufficient +pub fn require_role(user_role: R, required_role: R) -> Result<(), ValidationError> { + if user_role < required_role { + return Err(ValidationError::InsufficientRole); + } + Ok(()) +} + +/// Pattern for checking blacklist status +/// +/// # Arguments +/// * `is_blacklisted` - Whether the address is blacklisted +/// +/// # Errors +/// * `ValidationError::Blacklisted` - If address is blacklisted +pub fn require_not_blacklisted(is_blacklisted: bool) -> Result<(), ValidationError> { + if is_blacklisted { + return Err(ValidationError::Blacklisted); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::testutils::Address as _; + + #[test] + fn test_validate_amount() { + // Valid amounts + assert!(validate_amount(100, 1, 1000).is_ok()); + assert!(validate_amount(1, 1, 1000).is_ok()); + assert!(validate_amount(1000, 1, 1000).is_ok()); + + // Invalid amounts + assert_eq!(validate_amount(0, 1, 1000), Err(ValidationError::InvalidAmount)); + assert_eq!(validate_amount(-1, 1, 1000), Err(ValidationError::InvalidAmount)); + assert_eq!(validate_amount(100, 1, 50), Err(ValidationError::AmountTooLarge)); + assert_eq!(validate_amount(100, 200, 1000), Err(ValidationError::AmountTooSmall)); + } + + #[test] + fn test_validate_string() { + let env = Env::default(); + + // Valid strings + let short = String::from_str(&env, "hi"); + assert!(validate_string(short, 0, 10).is_ok()); + + let at_limit = String::from_str(&env, "1234567890"); + assert!(validate_string(at_limit, 0, 10).is_ok()); + + // Invalid strings + let empty = String::from_str(&env, ""); + assert_eq!(validate_string(empty.clone(), 1, 10), Err(ValidationError::StringTooShort)); + assert_eq!(validate_string(empty, 0, 10), Ok(())); + + let too_long = String::from_str(&env, "this string is way too long for the limit"); + assert_eq!(validate_string(too_long, 0, 10), Err(ValidationError::StringTooLong)); + } + + #[test] + fn test_validate_address() { + let env = Env::default(); + let address = Address::generate(&env); + + // Addresses are always valid in Soroban + assert!(validate_address(address).is_ok()); + } + + #[test] + fn test_validate_array() { + let env = Env::default(); + + // Valid arrays + let small = Vec::from_array(&env, [1, 2, 3]); + assert!(validate_array(small, 1, 10).is_ok()); + + let empty = Vec::from_array(&env, []); + assert!(validate_array(empty, 0, 10).is_ok()); + + // Invalid arrays + let too_small = Vec::from_array(&env, [1]); + assert_eq!(validate_array(too_small, 2, 10), Err(ValidationError::ArrayTooSmall)); + + let too_large = Vec::from_array(&env, [1, 2, 3, 4, 5, 6]); + assert_eq!(validate_array(too_large, 1, 3), Err(ValidationError::ArrayTooLarge)); + } +} \ No newline at end of file