|
| 1 | +use starknet::{ContractAddress, get_caller_address, get_block_timestamp}; |
| 2 | +use dojo::model::ModelStorage; |
| 3 | +use dojo::event::EventStorage; |
| 4 | +use dojo::world::WorldStorage; |
| 5 | +use coa::models::core::Contract; |
| 6 | +use coa::models::security::{ |
| 7 | + RateLimit, SecurityConfig, AdminRole, PlayerSecurityStatus, SecurityEvent, RateLimitExceeded, |
| 8 | + ContractPaused, ContractUnpaused, |
| 9 | +}; |
| 10 | +use coa::models::session::SessionKey; |
| 11 | +use core::num::traits::Zero; |
| 12 | +use core::poseidon::poseidon_hash_span; |
| 13 | + |
| 14 | +// Security constants |
| 15 | +pub const SUPER_ADMIN: felt252 = 'SUPER_ADMIN'; |
| 16 | +pub const GAME_ADMIN: felt252 = 'GAME_ADMIN'; |
| 17 | +pub const MODERATOR: felt252 = 'MODERATOR'; |
| 18 | + |
| 19 | +// Operation types as numbers for rate limiting |
| 20 | +pub const CREATE_SESSION_OP: u32 = 1; |
| 21 | +pub const SPAWN_ITEMS_OP: u32 = 2; |
| 22 | +pub const ADMIN_ACTION_OP: u32 = 3; |
| 23 | + |
| 24 | +pub const SECURITY_CONFIG_ID: felt252 = 'SECURITY_CONFIG'; |
| 25 | +pub const COA_CONTRACTS: felt252 = 'COA_CONTRACTS'; |
| 26 | + |
| 27 | +// Basic admin validation function (simplified) |
| 28 | +pub fn validate_admin_access(world: WorldStorage, _required_role: felt252) { |
| 29 | + let caller = get_caller_address(); |
| 30 | + |
| 31 | + // Validate caller is not zero address |
| 32 | + assert(!caller.is_zero(), 'ZERO_ADDRESS'); |
| 33 | + |
| 34 | + // Read contract state |
| 35 | + let contract: Contract = world.read_model(COA_CONTRACTS); |
| 36 | + |
| 37 | + // Validate contract state |
| 38 | + assert(!contract.admin.is_zero(), 'INVALID_CONTRACT_STATE'); |
| 39 | + assert(!contract.paused, 'CONTRACT_PAUSED'); |
| 40 | + |
| 41 | + // Basic admin check (simplified for now) |
| 42 | + assert(caller == contract.admin, 'INSUFFICIENT_PERMISSIONS'); |
| 43 | +} |
| 44 | + |
| 45 | +// Full admin validation function |
| 46 | +pub fn validate_admin_access_full(world: WorldStorage, required_role: felt252) { |
| 47 | + let caller = get_caller_address(); |
| 48 | + |
| 49 | + // Validate caller is not zero address |
| 50 | + assert(!caller.is_zero(), 'ZERO_ADDRESS'); |
| 51 | + |
| 52 | + // Read contract state |
| 53 | + let contract: Contract = world.read_model(COA_CONTRACTS); |
| 54 | + |
| 55 | + // Validate contract state |
| 56 | + assert(!contract.admin.is_zero(), 'INVALID_CONTRACT_STATE'); |
| 57 | + assert(!contract.paused, 'CONTRACT_PAUSED'); |
| 58 | + |
| 59 | + // Check if caller is super admin (contract admin) |
| 60 | + if caller == contract.admin { |
| 61 | + return; |
| 62 | + } |
| 63 | + |
| 64 | + // Check role-based access |
| 65 | + let admin_role: AdminRole = world.read_model(caller); |
| 66 | + assert(admin_role.is_active, 'INSUFFICIENT_PERMISSIONS'); |
| 67 | + |
| 68 | + // Validate role hierarchy using if-else |
| 69 | + if required_role == SUPER_ADMIN { |
| 70 | + assert(caller == contract.admin, 'SUPER_ADMIN_REQUIRED'); |
| 71 | + } else if required_role == GAME_ADMIN { |
| 72 | + assert( |
| 73 | + caller == contract.admin |
| 74 | + || admin_role.role_type == GAME_ADMIN |
| 75 | + || admin_role.role_type == SUPER_ADMIN, |
| 76 | + 'GAME_ADMIN_REQUIRED', |
| 77 | + ); |
| 78 | + } else if required_role == MODERATOR { |
| 79 | + assert( |
| 80 | + caller == contract.admin |
| 81 | + || admin_role.role_type == SUPER_ADMIN |
| 82 | + || admin_role.role_type == GAME_ADMIN |
| 83 | + || admin_role.role_type == MODERATOR, |
| 84 | + 'MODERATOR_REQUIRED', |
| 85 | + ); |
| 86 | + } else { |
| 87 | + assert(false, 'INVALID_ROLE'); |
| 88 | + } |
| 89 | +} |
| 90 | + |
| 91 | +pub fn validate_player_access(world: WorldStorage, player_id: ContractAddress) { |
| 92 | + let caller = get_caller_address(); |
| 93 | + |
| 94 | + // Validate caller is not zero address |
| 95 | + assert(!caller.is_zero(), 'ZERO_ADDRESS'); |
| 96 | + |
| 97 | + // Validate player ID matches caller (unless admin) |
| 98 | + let contract: Contract = world.read_model(COA_CONTRACTS); |
| 99 | + if caller != contract.admin { |
| 100 | + assert(caller == player_id, 'UNAUTHORIZED_PLAYER'); |
| 101 | + } |
| 102 | + |
| 103 | + // Check if player is banned |
| 104 | + let security_status: PlayerSecurityStatus = world.read_model(player_id); |
| 105 | + if security_status.is_banned { |
| 106 | + let current_time = get_block_timestamp(); |
| 107 | + if security_status.ban_expires_at > current_time { |
| 108 | + assert(false, 'PLAYER_BANNED'); |
| 109 | + } |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +pub fn check_rate_limit( |
| 114 | + mut world: WorldStorage, user: ContractAddress, operation_type: u32, |
| 115 | +) -> bool { |
| 116 | + let current_time = get_block_timestamp(); |
| 117 | + let time_window = current_time / 3600; // 1 hour windows |
| 118 | + let operation_felt: felt252 = operation_type.into(); |
| 119 | + let rate_limit_key = (user, operation_felt, time_window); |
| 120 | + |
| 121 | + let mut rate_limit: RateLimit = world.read_model(rate_limit_key); |
| 122 | + let security_config: SecurityConfig = world.read_model(SECURITY_CONFIG_ID); |
| 123 | + |
| 124 | + // Use if-else for operation type checking |
| 125 | + let limit = if operation_type == CREATE_SESSION_OP { |
| 126 | + security_config.max_sessions_per_hour |
| 127 | + } else if operation_type == SPAWN_ITEMS_OP { |
| 128 | + security_config.max_spawns_per_hour |
| 129 | + } else if operation_type == ADMIN_ACTION_OP { |
| 130 | + 50 // Default admin action limit |
| 131 | + } else { |
| 132 | + 100 // Default limit |
| 133 | + }; |
| 134 | + |
| 135 | + if rate_limit.count >= limit { |
| 136 | + // Emit rate limit exceeded event |
| 137 | + let event = RateLimitExceeded { |
| 138 | + user, |
| 139 | + operation: operation_felt, |
| 140 | + current_count: rate_limit.count, |
| 141 | + limit, |
| 142 | + timestamp: current_time, |
| 143 | + }; |
| 144 | + world.emit_event(@event); |
| 145 | + |
| 146 | + // Log security event |
| 147 | + let security_event = SecurityEvent { |
| 148 | + event_type: 'RATE_LIMIT_EXCEEDED', |
| 149 | + user, |
| 150 | + timestamp: current_time, |
| 151 | + details: operation_felt, |
| 152 | + }; |
| 153 | + world.emit_event(@security_event); |
| 154 | + |
| 155 | + return false; |
| 156 | + } |
| 157 | + |
| 158 | + rate_limit.count += 1; |
| 159 | + world.write_model(@rate_limit); |
| 160 | + true |
| 161 | +} |
| 162 | + |
| 163 | +pub fn sanitize_input(input: felt252) -> felt252 { |
| 164 | + // Basic input sanitization - in a real implementation, |
| 165 | + // you might want to check for malicious patterns |
| 166 | + input |
| 167 | +} |
| 168 | + |
| 169 | +pub fn validate_faction(faction: felt252) -> bool { |
| 170 | + faction == 'CHAOS_MERCENARIES' || faction == 'SUPREME_LAW' || faction == 'REBEL_TECHNOMANCERS' |
| 171 | +} |
| 172 | + |
| 173 | +pub fn validate_session_duration(duration: u64) -> bool { |
| 174 | + duration >= 3600 && duration <= 86400 // 1 hour to 24 hours |
| 175 | +} |
| 176 | + |
| 177 | +pub fn generate_secure_session_id(player: ContractAddress) -> felt252 { |
| 178 | + // Use multiple sources of entropy for security |
| 179 | + let tx_hash: felt252 = starknet::get_tx_info().unbox().transaction_hash; |
| 180 | + let block_timestamp: felt252 = get_block_timestamp().into(); |
| 181 | + let player_address: felt252 = player.into(); |
| 182 | + |
| 183 | + // Combine entropy sources |
| 184 | + let mut hash_data = array![tx_hash, block_timestamp, player_address]; |
| 185 | + poseidon_hash_span(hash_data.span()) |
| 186 | +} |
| 187 | + |
| 188 | +pub fn validate_contract_not_paused(world: WorldStorage) { |
| 189 | + let contract: Contract = world.read_model(COA_CONTRACTS); |
| 190 | + assert(!contract.paused, 'CONTRACT_PAUSED'); |
| 191 | +} |
| 192 | + |
| 193 | +pub fn log_security_event( |
| 194 | + mut world: WorldStorage, event_type: felt252, user: ContractAddress, details: felt252, |
| 195 | +) { |
| 196 | + let event = SecurityEvent { event_type, user, timestamp: get_block_timestamp(), details }; |
| 197 | + world.emit_event(@event); |
| 198 | +} |
| 199 | + |
| 200 | +// Emergency functions |
| 201 | +pub fn pause_contract(mut world: WorldStorage, reason: felt252) { |
| 202 | + validate_admin_access(world, SUPER_ADMIN); |
| 203 | + |
| 204 | + let mut contract: Contract = world.read_model(COA_CONTRACTS); |
| 205 | + contract.paused = true; |
| 206 | + world.write_model(@contract); |
| 207 | + |
| 208 | + let event = ContractPaused { |
| 209 | + paused_by: get_caller_address(), timestamp: get_block_timestamp(), reason, |
| 210 | + }; |
| 211 | + world.emit_event(@event); |
| 212 | +} |
| 213 | + |
| 214 | +pub fn unpause_contract(mut world: WorldStorage) { |
| 215 | + validate_admin_access(world, SUPER_ADMIN); |
| 216 | + |
| 217 | + let mut contract: Contract = world.read_model(COA_CONTRACTS); |
| 218 | + contract.paused = false; |
| 219 | + world.write_model(@contract); |
| 220 | + |
| 221 | + let event = ContractUnpaused { |
| 222 | + unpaused_by: get_caller_address(), timestamp: get_block_timestamp(), |
| 223 | + }; |
| 224 | + world.emit_event(@event); |
| 225 | +} |
| 226 | + |
| 227 | +// Session security helpers |
| 228 | +pub fn create_secure_session( |
| 229 | + mut world: WorldStorage, session_duration: u64, max_transactions: u32, |
| 230 | +) -> felt252 { |
| 231 | + let caller = get_caller_address(); |
| 232 | + let current_time = get_block_timestamp(); |
| 233 | + |
| 234 | + // Validate inputs |
| 235 | + assert(validate_session_duration(session_duration), 'INVALID_DURATION'); |
| 236 | + assert(max_transactions > 0 && max_transactions <= 1000, 'INVALID_TRANSACTIONS'); |
| 237 | + |
| 238 | + // Check rate limiting |
| 239 | + assert(check_rate_limit(world, caller, CREATE_SESSION_OP), 'RATE_LIMIT_EXCEEDED'); |
| 240 | + |
| 241 | + // Generate secure session ID |
| 242 | + let session_id = generate_secure_session_id(caller); |
| 243 | + |
| 244 | + // Create session with security measures |
| 245 | + let session_key = SessionKey { |
| 246 | + session_id, |
| 247 | + player_address: caller, |
| 248 | + session_key_address: caller, |
| 249 | + created_at: current_time, |
| 250 | + expires_at: current_time + session_duration, |
| 251 | + last_used: current_time, |
| 252 | + status: 0, // Active |
| 253 | + max_transactions, |
| 254 | + used_transactions: 0, |
| 255 | + is_valid: true, |
| 256 | + }; |
| 257 | + |
| 258 | + world.write_model(@session_key); |
| 259 | + session_id |
| 260 | +} |
| 261 | + |
| 262 | +// Input validation helpers |
| 263 | +pub fn validate_item_id(item_id: u256) -> bool { |
| 264 | + item_id > 0 |
| 265 | +} |
| 266 | + |
| 267 | +pub fn validate_quantity(quantity: u256) -> bool { |
| 268 | + quantity > 0 && quantity <= 1000000 // Reasonable upper limit |
| 269 | +} |
| 270 | + |
| 271 | +pub fn validate_address_not_zero(address: ContractAddress) -> bool { |
| 272 | + !address.is_zero() |
| 273 | +} |
0 commit comments