1use crate::contract::source::ContractSource;
16use regex::Regex;
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AccessControlMap {
22 pub ownership_pattern: Option<String>,
24 pub has_renounced_ownership: bool,
26 pub has_role_based_access: bool,
28 pub uses_tx_origin: bool,
30 pub tx_origin_locations: Vec<SourceLocation>,
32 pub modifiers: Vec<AccessModifier>,
34 pub privileged_functions: Vec<PrivilegedFunction>,
36 pub roles: Vec<String>,
38 pub auth_analysis: AuthAnalysis,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SourceLocation {
45 pub file: String,
47 pub line: usize,
49 pub snippet: String,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct AccessModifier {
56 pub name: String,
58 pub check_type: ModifierCheckType,
60 pub usage_count: usize,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub enum ModifierCheckType {
67 OwnerOnly,
69 RoleBased,
71 TxOrigin,
73 Custom,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct PrivilegedFunction {
80 pub name: String,
82 pub modifiers: Vec<String>,
84 pub capability: String,
86 pub risk: PrivilegeRisk,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum PrivilegeRisk {
93 Critical,
95 High,
97 Medium,
99 Low,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct AuthAnalysis {
106 pub msg_sender_checks: usize,
108 pub tx_origin_checks: usize,
110 pub has_origin_sender_comparison: bool,
112 pub summary: String,
114}
115
116pub fn analyze_access_control(source: &ContractSource) -> AccessControlMap {
118 let code = &source.source_code;
119
120 let ownership_pattern = detect_ownership_pattern(code);
121 let has_renounced_ownership = code.contains("renounceOwnership");
122 let has_role_based_access =
123 code.contains("AccessControl") || code.contains("hasRole") || code.contains("grantRole");
124 let uses_tx_origin = code.contains("tx.origin");
125 let tx_origin_locations = find_tx_origin_usage(code);
126 let modifiers = detect_modifiers(code);
127 let privileged_functions = detect_privileged_functions(code, &source.parsed_abi);
128 let roles = detect_roles(code);
129 let auth_analysis = analyze_auth_mechanisms(code);
130
131 AccessControlMap {
132 ownership_pattern,
133 has_renounced_ownership,
134 has_role_based_access,
135 uses_tx_origin,
136 tx_origin_locations,
137 modifiers,
138 privileged_functions,
139 roles,
140 auth_analysis,
141 }
142}
143
144fn detect_ownership_pattern(code: &str) -> Option<String> {
145 if code.contains("Ownable") {
146 Some("OpenZeppelin Ownable".to_string())
147 } else if code.contains("owner()") || code.contains("_owner") {
148 Some("Custom owner pattern".to_string())
149 } else if code.contains("AccessControl") {
150 Some("Role-based (AccessControl)".to_string())
151 } else {
152 None
153 }
154}
155
156fn find_tx_origin_usage(code: &str) -> Vec<SourceLocation> {
157 let mut locations = Vec::new();
158 for (line_num, line) in code.lines().enumerate() {
159 if line.contains("tx.origin") {
160 locations.push(SourceLocation {
161 file: String::new(),
162 line: line_num + 1,
163 snippet: line.trim().to_string(),
164 });
165 }
166 }
167 locations
168}
169
170fn detect_modifiers(code: &str) -> Vec<AccessModifier> {
171 let mut modifiers = Vec::new();
172
173 let re = Regex::new(r"modifier\s+(\w+)").unwrap();
175 for cap in re.captures_iter(code) {
176 let name = cap[1].to_string();
177
178 let check_type = if name.contains("onlyOwner") || name.contains("only_owner") {
179 ModifierCheckType::OwnerOnly
180 } else if name.contains("onlyRole")
181 || name.contains("only_role")
182 || name.contains("onlyAdmin")
183 {
184 ModifierCheckType::RoleBased
185 } else {
186 ModifierCheckType::Custom
187 };
188
189 let usage_pattern = format!(r"\b{}\b", regex::escape(&name));
191 let usage_re = Regex::new(&usage_pattern).unwrap();
192 let usage_count = usage_re.find_iter(code).count().saturating_sub(1); modifiers.push(AccessModifier {
195 name,
196 check_type,
197 usage_count,
198 });
199 }
200
201 if code.contains("onlyOwner") && !modifiers.iter().any(|m| m.name == "onlyOwner") {
203 let usage_re = Regex::new(r"\bonlyOwner\b").unwrap();
204 let usage_count = usage_re.find_iter(code).count();
205 modifiers.push(AccessModifier {
206 name: "onlyOwner".to_string(),
207 check_type: ModifierCheckType::OwnerOnly,
208 usage_count,
209 });
210 }
211
212 modifiers
213}
214
215fn detect_privileged_functions(
216 code: &str,
217 abi: &[crate::contract::source::AbiEntry],
218) -> Vec<PrivilegedFunction> {
219 let mut functions = Vec::new();
220
221 let patterns: Vec<(&str, &str, PrivilegeRisk)> = vec![
223 ("mint", "Mint/create new tokens", PrivilegeRisk::Critical),
224 ("burn", "Burn/destroy tokens", PrivilegeRisk::High),
225 ("pause", "Pause contract operations", PrivilegeRisk::High),
226 (
227 "unpause",
228 "Unpause contract operations",
229 PrivilegeRisk::High,
230 ),
231 ("setFee", "Modify fee parameters", PrivilegeRisk::Medium),
232 ("setPrice", "Modify price parameters", PrivilegeRisk::Medium),
233 (
234 "withdraw",
235 "Withdraw funds from contract",
236 PrivilegeRisk::Critical,
237 ),
238 (
239 "transferOwnership",
240 "Transfer contract ownership",
241 PrivilegeRisk::Critical,
242 ),
243 (
244 "upgradeTo",
245 "Upgrade contract implementation",
246 PrivilegeRisk::Critical,
247 ),
248 ("selfdestruct", "Destroy contract", PrivilegeRisk::Critical),
249 ("blacklist", "Blacklist addresses", PrivilegeRisk::High),
250 ("whitelist", "Whitelist addresses", PrivilegeRisk::Medium),
251 ("setOracle", "Change price oracle", PrivilegeRisk::Critical),
252 ("setRouter", "Change DEX router", PrivilegeRisk::Critical),
253 ];
254
255 let fn_name_re = Regex::new(r"function\s+(\w+)").unwrap();
256
257 for (pattern, capability, risk) in &patterns {
258 let fn_re = Regex::new(&format!(
260 r"function\s+\w*{}\w*\s*\([^)]*\)[^{{]*\b(onlyOwner|onlyRole|onlyAdmin|whenNotPaused)\b",
261 regex::escape(pattern)
262 ));
263 if let Ok(re) = fn_re {
264 for cap in re.captures_iter(code) {
265 let full_match = cap.get(0).map_or("", |m| m.as_str());
266 if let Some(fn_cap) = fn_name_re.captures(full_match) {
267 let fn_name = fn_cap[1].to_string();
268 let modifier = cap[1].to_string();
269 functions.push(PrivilegedFunction {
270 name: fn_name,
271 modifiers: vec![modifier],
272 capability: capability.to_string(),
273 risk: risk.clone(),
274 });
275 }
276 }
277 }
278
279 let pattern_lower = pattern.to_lowercase();
281 for entry in abi {
282 if entry.entry_type == "function"
283 && entry.name.to_lowercase().contains(&pattern_lower)
284 && entry.is_state_changing()
285 && !functions.iter().any(|f| f.name == entry.name)
286 {
287 functions.push(PrivilegedFunction {
288 name: entry.name.clone(),
289 modifiers: vec!["(from ABI)".to_string()],
290 capability: capability.to_string(),
291 risk: risk.clone(),
292 });
293 }
294 }
295 }
296
297 functions
298}
299
300fn detect_roles(code: &str) -> Vec<String> {
301 let mut roles = Vec::new();
302
303 let re = Regex::new(r#"(?:bytes32|constant)\s+.*?(\w+_ROLE)\s*="#).unwrap();
306 for cap in re.captures_iter(code) {
307 roles.push(cap[1].to_string());
308 }
309
310 for role in &[
312 "DEFAULT_ADMIN_ROLE",
313 "MINTER_ROLE",
314 "PAUSER_ROLE",
315 "BURNER_ROLE",
316 "UPGRADER_ROLE",
317 ] {
318 if code.contains(role) && !roles.contains(&role.to_string()) {
319 roles.push(role.to_string());
320 }
321 }
322
323 roles
324}
325
326fn analyze_auth_mechanisms(code: &str) -> AuthAnalysis {
327 let msg_sender_re = Regex::new(r"msg\.sender").unwrap();
328 let tx_origin_re = Regex::new(r"tx\.origin").unwrap();
329 let origin_sender_re =
330 Regex::new(r"(?:tx\.origin\s*==\s*msg\.sender|msg\.sender\s*==\s*tx\.origin)").unwrap();
331
332 let msg_sender_checks = msg_sender_re.find_iter(code).count();
333 let tx_origin_checks = tx_origin_re.find_iter(code).count();
334 let has_origin_sender_comparison = origin_sender_re.is_match(code);
335
336 let summary = if tx_origin_checks > 0 && !has_origin_sender_comparison {
337 format!(
338 "DANGER: Uses tx.origin ({} occurrence(s)) without msg.sender comparison. \
339 This is vulnerable to phishing attacks via malicious contracts.",
340 tx_origin_checks
341 )
342 } else if has_origin_sender_comparison {
343 "Uses tx.origin == msg.sender comparison (anti-contract-call guard). \
344 Less risky but blocks legitimate contract interactions."
345 .to_string()
346 } else if msg_sender_checks > 0 {
347 format!(
348 "Uses msg.sender for authorization ({} check(s)). This is the recommended approach.",
349 msg_sender_checks
350 )
351 } else {
352 "No explicit authorization checks detected.".to_string()
353 };
354
355 AuthAnalysis {
356 msg_sender_checks,
357 tx_origin_checks,
358 has_origin_sender_comparison,
359 summary,
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::contract::source::ContractSource;
367
368 fn make_source(code: &str) -> ContractSource {
369 ContractSource {
370 contract_name: "Test".to_string(),
371 source_code: code.to_string(),
372 abi: "[]".to_string(),
373 compiler_version: "v0.8.19".to_string(),
374 optimization_used: true,
375 optimization_runs: 200,
376 evm_version: "paris".to_string(),
377 license_type: "MIT".to_string(),
378 is_proxy: false,
379 implementation_address: None,
380 constructor_arguments: String::new(),
381 library: String::new(),
382 swarm_source: String::new(),
383 parsed_abi: vec![],
384 }
385 }
386
387 #[test]
388 fn test_detect_ownable() {
389 let src = make_source("contract Token is Ownable { function mint() onlyOwner {} }");
390 let ac = analyze_access_control(&src);
391 assert_eq!(
392 ac.ownership_pattern,
393 Some("OpenZeppelin Ownable".to_string())
394 );
395 assert!(!ac.has_renounced_ownership);
396 }
397
398 #[test]
399 fn test_detect_renounced_ownership() {
400 let src = make_source("contract Token { function renounceOwnership() {} }");
401 let ac = analyze_access_control(&src);
402 assert!(ac.has_renounced_ownership);
403 }
404
405 #[test]
406 fn test_detect_tx_origin() {
407 let src = make_source("require(tx.origin == owner, 'not owner');");
408 let ac = analyze_access_control(&src);
409 assert!(ac.uses_tx_origin);
410 assert_eq!(ac.tx_origin_locations.len(), 1);
411 }
412
413 #[test]
414 fn test_detect_roles() {
415 let src = make_source(
416 "bytes32 public constant MINTER_ROLE = keccak256('MINTER_ROLE');\n\
417 bytes32 public constant PAUSER_ROLE = keccak256('PAUSER_ROLE');",
418 );
419 let ac = analyze_access_control(&src);
420 assert!(ac.roles.contains(&"MINTER_ROLE".to_string()));
421 assert!(ac.roles.contains(&"PAUSER_ROLE".to_string()));
422 }
423
424 #[test]
425 fn test_detect_access_control() {
426 let src = make_source(
427 "import AccessControl; contract Token is AccessControl { \
428 function mint() onlyRole(MINTER_ROLE) {} }",
429 );
430 let ac = analyze_access_control(&src);
431 assert!(ac.has_role_based_access);
432 }
433
434 #[test]
435 fn test_auth_analysis_safe() {
436 let src = make_source("require(msg.sender == owner);");
437 let ac = analyze_access_control(&src);
438 assert_eq!(ac.auth_analysis.msg_sender_checks, 1);
439 assert_eq!(ac.auth_analysis.tx_origin_checks, 0);
440 assert!(ac.auth_analysis.summary.contains("recommended approach"));
441 }
442
443 #[test]
444 fn test_auth_analysis_dangerous() {
445 let src = make_source("require(tx.origin == owner);");
446 let ac = analyze_access_control(&src);
447 assert!(ac.auth_analysis.summary.contains("DANGER"));
448 }
449
450 #[test]
451 fn test_detect_ownership_pattern_custom_owner() {
452 let result = detect_ownership_pattern(
453 "function owner() public view returns (address) { return _owner; }",
454 );
455 assert_eq!(result, Some("Custom owner pattern".to_string()));
456 }
457
458 #[test]
459 fn test_detect_ownership_pattern_none() {
460 let result = detect_ownership_pattern("contract SimpleToken { function transfer() {} }");
461 assert_eq!(result, None);
462 }
463
464 #[test]
465 fn test_detect_modifiers_custom() {
466 let code = "modifier onlyValidator() { require(isValidator[msg.sender]); _; }\nfunction doThing() onlyValidator() {}";
467 let modifiers = detect_modifiers(code);
468 assert!(modifiers.iter().any(|m| m.name == "onlyValidator"));
469 let validator_mod = modifiers
470 .iter()
471 .find(|m| m.name == "onlyValidator")
472 .unwrap();
473 assert!(matches!(
474 validator_mod.check_type,
475 ModifierCheckType::Custom
476 ));
477 }
478
479 #[test]
480 fn test_detect_modifiers_role_based() {
481 let code = "modifier onlyRole(bytes32 role) { _checkRole(role); _; }\nfunction mint() onlyRole(MINTER) {}";
482 let modifiers = detect_modifiers(code);
483 assert!(modifiers.iter().any(|m| m.name == "onlyRole"));
484 let role_mod = modifiers.iter().find(|m| m.name == "onlyRole").unwrap();
485 assert!(matches!(role_mod.check_type, ModifierCheckType::RoleBased));
486 }
487
488 #[test]
489 fn test_detect_modifiers_admin() {
490 let code = "modifier onlyAdmin() { require(msg.sender == admin); _; }";
491 let modifiers = detect_modifiers(code);
492 assert!(modifiers.iter().any(|m| m.name == "onlyAdmin"));
493 let admin_mod = modifiers.iter().find(|m| m.name == "onlyAdmin").unwrap();
494 assert!(matches!(admin_mod.check_type, ModifierCheckType::RoleBased));
495 }
496
497 #[test]
498 fn test_detect_modifiers_imported_only_owner() {
499 let code =
500 "function mint() onlyOwner { tokens[msg.sender] += 1; }\nfunction burn() onlyOwner {}";
501 let modifiers = detect_modifiers(code);
502 assert!(modifiers.iter().any(|m| m.name == "onlyOwner"));
503 let owner_mod = modifiers.iter().find(|m| m.name == "onlyOwner").unwrap();
504 assert!(matches!(owner_mod.check_type, ModifierCheckType::OwnerOnly));
505 assert!(owner_mod.usage_count >= 2);
506 }
507
508 #[test]
509 fn test_detect_privileged_functions_with_abi() {
510 use crate::contract::source::{AbiEntry, AbiParam};
511 let code = "function mint(address to) onlyOwner { _mint(to); }";
512 let abi = vec![
513 AbiEntry {
514 entry_type: "function".to_string(),
515 name: "mint".to_string(),
516 inputs: vec![AbiParam {
517 name: "to".to_string(),
518 param_type: "address".to_string(),
519 indexed: false,
520 components: vec![],
521 }],
522 outputs: vec![],
523 state_mutability: "nonpayable".to_string(),
524 },
525 AbiEntry {
526 entry_type: "function".to_string(),
527 name: "pause".to_string(),
528 inputs: vec![],
529 outputs: vec![],
530 state_mutability: "nonpayable".to_string(),
531 },
532 ];
533 let fns = detect_privileged_functions(code, &abi);
534 assert!(!fns.is_empty());
535 assert!(fns.iter().any(|f| f.name == "mint"));
536 }
537
538 #[test]
539 fn test_detect_privileged_functions_abi_only() {
540 use crate::contract::source::{AbiEntry, AbiParam};
541 let code = "contract Token {}";
542 let abi = vec![AbiEntry {
543 entry_type: "function".to_string(),
544 name: "setFeeRecipient".to_string(),
545 inputs: vec![AbiParam {
546 name: "r".to_string(),
547 param_type: "address".to_string(),
548 indexed: false,
549 components: vec![],
550 }],
551 outputs: vec![],
552 state_mutability: "nonpayable".to_string(),
553 }];
554 let fns = detect_privileged_functions(code, &abi);
555 assert!(fns.iter().any(|f| f.name == "setFeeRecipient"));
556 }
557
558 #[test]
559 fn test_detect_roles_common_patterns() {
560 let code = "contract Token {\n\
561 bytes32 public constant UPGRADER_ROLE = keccak256('UPGRADER_ROLE');\n\
562 DEFAULT_ADMIN_ROLE;\n\
563 BURNER_ROLE;\n\
564 }";
565 let roles = detect_roles(code);
566 assert!(roles.contains(&"UPGRADER_ROLE".to_string()));
567 assert!(roles.contains(&"DEFAULT_ADMIN_ROLE".to_string()));
568 assert!(roles.contains(&"BURNER_ROLE".to_string()));
569 }
570
571 #[test]
572 fn test_analyze_auth_tx_origin_with_msg_sender() {
573 let code = "require(tx.origin == msg.sender, 'no contracts');";
574 let auth = analyze_auth_mechanisms(code);
575 assert!(auth.has_origin_sender_comparison);
576 assert!(auth.summary.contains("anti-contract-call"));
577 }
578
579 #[test]
580 fn test_analyze_auth_no_checks() {
581 let code = "contract Token { function transfer() {} }";
582 let auth = analyze_auth_mechanisms(code);
583 assert_eq!(auth.msg_sender_checks, 0);
584 assert_eq!(auth.tx_origin_checks, 0);
585 assert!(auth.summary.contains("No explicit authorization"));
586 }
587
588 #[test]
589 fn test_find_tx_origin_usage_multiple() {
590 let code = "require(tx.origin == owner);\nrequire(tx.origin != address(0));";
591 let locations = find_tx_origin_usage(code);
592 assert_eq!(locations.len(), 2);
593 assert_eq!(locations[0].line, 1);
594 assert_eq!(locations[1].line, 2);
595 }
596
597 #[test]
598 fn test_privilege_risk_debug() {
599 assert_eq!(format!("{:?}", PrivilegeRisk::Critical), "Critical");
600 assert_eq!(format!("{:?}", PrivilegeRisk::High), "High");
601 assert_eq!(format!("{:?}", PrivilegeRisk::Medium), "Medium");
602 assert_eq!(format!("{:?}", PrivilegeRisk::Low), "Low");
603 }
604
605 #[test]
606 fn test_modifier_check_type_debug() {
607 assert_eq!(format!("{:?}", ModifierCheckType::OwnerOnly), "OwnerOnly");
608 assert_eq!(format!("{:?}", ModifierCheckType::RoleBased), "RoleBased");
609 assert_eq!(format!("{:?}", ModifierCheckType::Custom), "Custom");
610 }
611}