Variant Analysis
Multiply the value of a single vulnerability finding by systematically locating similar issues elsewhere in the codebase. Once you find one bug, this skill helps you find all its "cousins."
When to Use
-
After discovering an initial vulnerability
-
During comprehensive security audits
-
When creating detection patterns for CI/CD
-
Before claiming a bug class is fully remediated
-
When assessing the extent of a vulnerability pattern
- The Variant Analysis Process
From Finding to Pattern
- Find Initial Bug → Specific vulnerability instance
- Abstract Pattern → What makes this a bug?
- Create Search → Grep/Semgrep queries
- Find Variants → All similar occurrences
- Verify Each → Confirm true positives
- Report All → Document the bug class
Example Transformation
Initial Bug:
// Found: Missing payment validation in deposit() #[payable("*")] fn deposit(&self) { let payment = self.call_value().single_esdt(); self.balances().update(|b| *b += payment.amount); // Bug: No token ID validation! }
Abstract Pattern:
-
#[payable("*")] endpoint
-
Uses call_value() to get payment
-
Does NOT validate token_identifier
Search Query:
Find all payable endpoints
grep -n "#[payable" src/*.rs
Then check each for token validation
grep -A 20 "#[payable" src/*.rs | grep -v "token_identifier"
- Common MultiversX Variant Patterns
Pattern: Missing Payment Validation
Initial Finding: One endpoint accepts payment but doesn't validate the token.
Variant Search:
Find all payable endpoints
grep -rn "#[payable" src/
Check for missing token validation
Look for call_value() without subsequent token_identifier check
grep -A 30 "#[payable" src/*.rs > payable_endpoints.txt
Manually review each for token_identifier validation
Semgrep Rule:
rules:
- id: mvx-payable-no-token-check
patterns:
- pattern: | #[payable("*")] $ANNOTATIONS fn $FUNC(&self, $...PARAMS) { $...BODY }
- pattern-not: | #[payable("*")] $ANNOTATIONS fn $FUNC(&self, $...PARAMS) { <... token_identifier ...> }
Pattern: Unbounded Iteration
Initial Finding: One function iterates over a VecMapper without bounds.
Variant Search:
Find all .iter() calls on storage mappers
grep -rn ".iter()" src/
Find all for loops over storage
grep -rn "for.*in.*self." src/
Checklist for Each:
-
Is iteration bounded?
-
Can a user grow the collection?
-
Is there pagination?
Pattern: Callback State Assumptions
Initial Finding: One callback doesn't handle the error case.
Variant Search:
Find all callbacks
grep -rn "#[callback]" src/
Check for proper result handling
grep -A 20 "#[callback]" src/*.rs | grep -c "ManagedAsyncCallResult"
All Callbacks Need:
#[callback] fn any_callback(&self, #[call_result] result: ManagedAsyncCallResult<T>) { match result { ManagedAsyncCallResult::Ok() => { /* success */ }, ManagedAsyncCallResult::Err() => { /* handle failure! */ } } }
Pattern: Missing Access Control
Initial Finding: One admin function lacks #[only_owner] .
Variant Search:
Find functions that modify admin-like storage
grep -rn "admin|owner|config|fee" src/ | grep ".set("
Cross-reference with access control
grep -B 10 "admin..set|config..set" src/*.rs | grep -v "only_owner"
Pattern: Arithmetic Without Checks
Initial Finding: One calculation uses raw + instead of checked_add .
Variant Search:
Find all arithmetic operations
grep -rn " + | - | * " src/*.rs
Exclude test files and comments
grep -rn " + | - | * " src/*.rs | grep -v "test|//"
- Systematic Variant Hunting
Step 1: Characterize the Bug
Answer these questions:
-
What is the vulnerable code pattern?
-
What makes it exploitable?
-
What would a fix look like?
Step 2: Create Detection Queries
Grep-based:
Pattern: [specific code pattern]
grep -rn "[pattern]" src/
Negative pattern (should be present but isn't)
grep -L "[expected_pattern]" src/*.rs
Semgrep-based:
See multiversx-semgrep-creator skill for details
rules:
- id: variant-pattern
patterns:
- pattern: <vulnerable pattern>
- pattern-not: <fixed pattern>
Step 3: Triage Results
For each potential variant:
Result Classification Action
Clearly vulnerable True Positive Report
Needs context Investigate Manual review
Has mitigation False Positive Document why
Different pattern Not a variant Skip
Step 4: Document Findings
Variant Analysis: [Bug Class Name]
Initial Finding
- Location: [file:line]
- Description: [what's wrong]
Pattern Description
[Abstract description of what makes this a bug]
Search Method
[grep/semgrep commands used]
Variants Found
Location
Status
Notes
file1.rs:23
Confirmed
Same pattern
file2.rs:45
Confirmed
Slight variation
file3.rs:67
FP
Has validation elsewhere
Remediation
[How to fix all instances]
## 4. Automation for Future Prevention
### Convert to CI/CD Check
After finding variants, create automated detection:
```yaml
# .github/workflows/security.yml
- name: Check for vulnerability patterns
run: |
# Run semgrep with custom rules
semgrep --config rules/mvx-security.yaml src/
# Grep-based checks
if grep -rn "unsafe_pattern" src/; then
echo "Found potential vulnerability"
exit 1
fi
Create Semgrep Rule
See multiversx-semgrep-creator
skill:
rules:
- id: mvx-[bug-class]-[id]
languages: [rust]
message: "[Description of bug class]"
severity: ERROR
patterns:
- pattern: <vulnerable pattern>
5. Variant Analysis Checklist
After finding any bug:
- Abstract the pattern (what makes it a bug?)
- Create search queries (grep, semgrep)
- Search entire codebase
- Triage each result (TP/FP/needs investigation)
- Verify true positives are exploitable
- Document all variants
- Create prevention rule for CI/CD
- Recommend fix for all instances
6. Common Variant Categories
Input Validation Variants
- Missing in one endpoint → Check ALL endpoints
- Missing for one parameter → Check ALL parameters
Access Control Variants
- Missing on one admin function → Check ALL admin functions
- Inconsistent role checks → Audit entire role system
State Management Variants
- Reentrancy in one function → Check ALL external calls
- Missing callback handling → Check ALL callbacks
Arithmetic Variants
- Overflow in one calculation → Check ALL math operations
- Precision loss in one formula → Check ALL division operations
7. Reporting Multiple Variants
Consolidated Report
When multiple variants exist, consolidate:
# Bug Class: [Name]
## Summary
Found [N] instances of [bug description] across the codebase.
## Root Cause
[Why this pattern is vulnerable]
## Instances
### Instance 1 (file1.rs:23)
[Details]
### Instance 2 (file2.rs:45)
[Details]
...
## Recommended Fix
[Generic fix pattern]
```rust
// Before (vulnerable)
[vulnerable code]
// After (fixed)
[fixed code]
Prevention
[How to prevent this class of bugs in the future]
### Severity Aggregation
| Individual Severity | Count | Aggregate Severity |
|---------------------|-------|-------------------|
| Critical | 3+ | Critical |
| High | 5+ | Critical |
| Medium | 10+ | High |
| Low | Any | Low |
## 8. Example: Complete Variant Analysis
**Initial Bug: Missing amount validation in stake()**
```rust
// Found in stake.rs:45
#[payable("EGLD")]
fn stake(&self) {
let payment = self.call_value().egld_value();
// Bug: No check for amount > 0
self.staked().update(|s| *s += payment.clone_value());
}
Pattern: Missing amount > 0 check on payable endpoint
Search:
grep -rn "#\[payable" src/ | cut -d: -f1 | sort -u | while read file; do
echo "=== $file ==="
grep -A 30 "#\[payable" "$file" | head -40
done > payable_review.txt
Variants Found:
- stake.rs:45
- stake() - CONFIRMED
- stake.rs:78
- add_stake() - CONFIRMED
- rewards.rs:23
- deposit_rewards() - CONFIRMED
- fees.rs:12
- pay_fee() - FALSE POSITIVE (has check on line 15)
Fix Applied to All:
#[payable("EGLD")]
fn stake(&self) {
let payment = self.call_value().egld_value();
require!(payment.clone_value() > 0, "Amount must be positive");
self.staked().update(|s| *s += payment.clone_value());
}
CI Rule Created: rules/mvx-amount-validation.yaml