use lazy_static::lazy_static;
use regex::Regex;
use sentri_core::Finding;
lazy_static! {
static ref DVN_ARRAY_REGEX: Regex =
Regex::new(r"(?i)(dvn|verifier|validator)\s*(\[|\(|:)").unwrap();
static ref DVN_COUNT_REGEX: Regex =
Regex::new(r"(?i)(require|assert)\s*\(\s*dvn.*\.length|dvnCount|num.*dvn").unwrap();
static ref DVN_SETTER_REGEX: Regex =
Regex::new(r"(?i)function\s+(set|add|update|configure)\s*DVN").unwrap();
}
pub fn detect_dvn_single_point_failure(source: &str, file_path: &str) -> Vec<Finding> {
let mut findings = Vec::new();
for (func_line_num, func_line) in source.lines().enumerate() {
if !DVN_SETTER_REGEX.is_match(func_line)
&& !func_line.to_lowercase().contains("setdvn")
&& !func_line.to_lowercase().contains("adddvn")
{
continue;
}
let func_name = extract_function_name(func_line);
let func_start = func_line_num;
let func_end = (func_line_num + 30).min(source.lines().count());
let func_body = source
.lines()
.skip(func_start)
.take(func_end - func_start)
.collect::<Vec<&str>>()
.join("\n");
if allows_single_dvn(&func_body) {
let message = format!(
"DVN configuration function '{}' allows single DVN without minimum count enforcement. \
This creates a critical single point of failure. H47 KelpDAO ($292M) was exploited \
when only 1 DVN was configured, causing a complete bridge compromise. \
\
An attacker controlling or compromising the single DVN could forge any cross-chain message. \
\
Required fix: Enforce minimum DVN count: \
require(dvns.length >= 2, \"Minimum 2 DVNs required\"); \
Recommended: Require at least 5 independent DVNs for production.",
func_name
);
findings.push(
Finding::new(
"evm_dvn_single_point_failure".to_string(),
sentri_core::Severity::Critical,
file_path.to_string(),
func_line_num + 1,
0,
message,
func_line.trim().to_string(),
)
.with_metadata("exploit_id".to_string(), "H47".to_string())
.with_metadata("exploit_name".to_string(), "KelpDAO/LayerZero".to_string())
.with_metadata("loss".to_string(), "$292M".to_string())
.with_metadata("year".to_string(), "2026".to_string())
.with_metadata(
"vulnerability_type".to_string(),
"single_point_failure".to_string(),
)
.with_metadata(
"detector".to_string(),
"configuration_validation".to_string(),
)
.with_source_fragment(func_body),
);
}
}
findings
}
fn extract_function_name(line: &str) -> String {
if let Some(start) = line.find("function ") {
let after_function = &line[start + 9..];
if let Some(end) = after_function.find('(') {
return after_function[..end].trim().to_string();
}
}
"setDVN".to_string()
}
fn allows_single_dvn(func_body: &str) -> bool {
let func_lower = func_body.to_lowercase();
let has_dvn_operation = func_lower.contains("dvn[")
|| func_lower.contains("dvns.push")
|| func_lower.contains("dvns =")
|| func_lower.contains("dvn_list")
|| func_lower.contains("verifiers");
if !has_dvn_operation {
return false;
}
let has_minimum_check = func_lower.contains("length >=")
|| func_lower.contains("length > 0")
|| func_lower.contains("require(dvn")
|| func_lower.contains("require(verifier")
|| func_lower.contains("dvn_count")
|| func_lower.contains("min_dvn");
!has_minimum_check
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vulnerable_single_dvn_kelp_dao() {
let code = r#"
contract LayerZeroBridge {
address[] public dvns;
function setDVN(address newDVN) external onlyAdmin {
dvns = new address[](1);
dvns[0] = newDVN; // VULNERABLE: Only 1 DVN allowed!
}
function verify(bytes calldata proof) external view returns (bool) {
return dvns[0].call(proof); // Single point of failure!
}
}
"#;
let findings = detect_dvn_single_point_failure(code, "bridge.sol");
assert!(
!findings.is_empty(),
"Should detect single DVN vulnerability"
);
assert!(findings[0].invariant_id.contains("dvn_single_point"));
}
#[test]
fn test_vulnerable_unconstrained_dvn() {
let code = r#"
contract OFT {
address[] public dvns;
function addDVN(address dvn) external onlyAdmin {
dvns.push(dvn); // No minimum check!
}
}
"#;
let findings = detect_dvn_single_point_failure(code, "oft.sol");
assert!(!findings.is_empty(), "Should detect unconstrained DVN");
}
#[test]
fn test_safe_with_minimum_dvn_check() {
let code = r#"
contract SafeBridge {
address[] public dvns;
uint constant MIN_DVNS = 3;
function setDVN(address[] calldata newDVNs) external onlyAdmin {
require(newDVNs.length >= MIN_DVNS, "Insufficient DVN count");
dvns = newDVNs;
}
}
"#;
let findings = detect_dvn_single_point_failure(code, "bridge.sol");
assert!(
findings.is_empty(),
"Should not flag when minimum DVN check present"
);
}
#[test]
fn test_safe_with_inline_length_check() {
let code = r#"
contract SafeBridge {
address[] public verifiers;
function setVerifiers(address[] calldata newVerifiers) external {
require(newVerifiers.length >= 2, "Need at least 2 verifiers");
verifiers = newVerifiers;
}
}
"#;
let findings = detect_dvn_single_point_failure(code, "bridge.sol");
assert!(findings.is_empty(), "Should detect length >= 2 check");
}
#[test]
fn test_vulnerable_no_check_pattern() {
let code = r#"
contract VulnerableBridge {
address[] public dvn_list;
function configureDVN(address[] calldata dvns) external {
dvn_list = dvns; // No validation at all!
}
}
"#;
let findings = detect_dvn_single_point_failure(code, "bridge.sol");
assert!(
!findings.is_empty(),
"Should detect unconstrained configuration"
);
}
}