Comprehensive MultiversX smart contract security audit. Vulnerability patterns, ESDT safety, async callbacks, DeFi analysis, storage lifecycle, static analysis, Semgrep scanning, test verification. Use when performing security audits, code reviews, or setting up automated vulnerability detection.
Instalação
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Pattern D — Removal doesn't clear in-flight state:
// BUG: Removing item but not clearing associated pending statefnremove_token(&self, token: TokenIdentifier) {
self.token_whitelist().swap_remove(&token);
// MISSING: What about accumulated_fees for this token in current period?// Those become stuck - can't be claimed (not in whitelist)// and can't be withdrawn (still reserved)
}
Orphans tokens for the current time period.
Pattern E — Permissionless deposit of special tokens:
// BUG: Anyone can deposit XMEX without energy deduction#[payable("*")]fndeposit_swap_fees(&self) {
letpayment = self.call_value().single_esdt();
// MISSING: If payment is XMEX, verify caller is authorized (e.g., Token Unstake SC)// Otherwise sender's energy isn't deducted but recipients gain energy
}
Breaks energy conservation — total energy inflates.
Pattern F — Storage key removed from code but not cleaned (CONTEXT-DEPENDENT):
// NOTE: On MultiversX, empty upgrade() is the STANDARD pattern.// Orphaned storage keys from removed mappers are just wasted space —// the framework never reads them unless code references them.//// This is ONLY a real issue when:// 1. The old storage key name is reused by a NEW mapper with different semantics// 2. A storage format change requires data migration (e.g., struct fields added/removed)// 3. The old data is actively read by remaining code via raw storage access//// Do NOT flag empty upgrade() as a vulnerability by default.// Do NOT flag orphaned storage keys as a vulnerability unless collision/migration is needed.
Only check git history when there is reason to believe key name collision or data migration is needed.
Pattern G — Unbounded collection without size limit:
// BUG: Collection can grow indefinitely#[only_owner]fnadd_to_whitelist(&self, items: MultiValueEncoded<Item>) {
foritemin items {
self.whitelist().insert(item);
// MISSING: require!(self.whitelist().len() < MAX_SIZE)
}
}
// Later, iteration over whitelist can exceed gas limits
Any collection iterated in user-facing functions needs a max size.
Pattern H — Endpoint exists but planned for removal:
// BUG: Unnecessary endpoint increasing attack surface#[only_owner]#[endpoint(addRewardTokens)]fnadd_reward_tokens(&self, tokens: MultiValueEncoded<TokenIdentifier>) {
// Is it actually needed after initial setup?// Is there a plan to remove all tokens except base tokens?
}
Check: Is this endpoint needed? Look for matching remove without add being used.
Pattern I — Returns zero for early weeks (OFTEN MISSED):
// BUG: No swapping/claiming possible for first N weeksfnget_available(&self) -> BigUint {
if current_week < THRESHOLD {
return BigUint::zero(); // Blocks functionality for first N weeks!
}
// ... rest of calculation
}
CHECK: Is this intentional? Does it block admin functions like swap_token_to_base_token?
SEARCH FOR: if current_week < or if current_epoch < patterns.
Pattern J — Removal orphans multi-period state (OFTEN MISSED):
// BUG: Removal makes claimable periods inaccessiblefnremove_token(&self, token: TokenIdentifier) {
self.reward_tokens().swap_remove(&token);
// accumulated_fees for CURRENT period is stuck (Pattern D)// BUT ALSO: What about the NEXT K periods where users can still claim?// If claim window is 4 weeks, users lose access to 4 weeks of rewards!
}
For any removal, calculate: How many time periods of data become inaccessible?
Decimal mismatch: mixed 6-decimal (USDC) and 18-decimal (EGLD) tokens.
Critical Path Verification (MANDATORY)
YOU MUST COMPLETE THIS SECTION — DO NOT SKIP
For reward/fee distribution contracts, trace these flows end-to-end:
1. Token Deposit → Storage → Claim Flow:
Q: When tokens are deposited, where do they go in storage?
Q: When the first claim happens for a week, does storage move/transform?
Q: Does the "available balance" calculation account for BOTH locations?
2. Balance Calculation Audit:
Find ALL functions that calculate "available" or "claimable" amounts.
For EACH function, verify it subtracts ALL reserved/committed amounts.
Specific check: If there's accumulated_fees AND total_rewards_for_week, does the available calculation subtract BOTH?
3. Token Removal Flow:
What happens to tokens in various storage locations when a reward token is removed?
Can removal orphan committed rewards?
Can base tokens (MEX/XMEX) be removed?
What happens to claims for the NEXT N periods after removal?
4. Time Gap Scenarios:
What if no interaction for multiple weeks?
Does each missed week get handled, or only the most recent?
What about week 0 / first weeks edge cases?
5. Permissionless Endpoint Abuse:
For each public endpoint, what tokens can be sent?
If XMEX can be sent, is the sender's energy properly deducted?
Can someone inflate totals by depositing and claiming?
6. Early Period Blocking:
Do any functions return early/zero for the first N periods?
Is this blocking intentional or does it break functionality?
Can users/admins work around it?
Cross-Cutting Checks (G1-G8)
Use as a final sweep — each should already be covered above. Verify nothing was missed:
Check
Key Question
G1: Admin Cascading
What breaks N periods later? Quantify: K periods.
G2: Storage Lifecycle
Is removed mapper cleaned? Must check git history.
G3: Unbounded Collections
Max size + iteration?
G4: Dead Code
Is add_X used after init? Check for add/remove pairs.
G5: Time-Delayed
Admin action during validity? Early period blocking?
Generate Mandos scenarios from any failures found.
Phase 6: Post-Discovery
Variant Analysis
After finding any vulnerability, use mvx-variant-analysis to:
Abstract the bug to a general pattern.
Search for all instances across the codebase.
Create Semgrep rules using mvx-semgrep-creator for CI/CD prevention.
Fix Verification
When fixes are proposed, use mvx-fix-verification to:
Reproduce the original bug with a test.
Verify the fix makes the exploit test fail.
Run regression suite to confirm no side effects.
Severity Calibration
Owner Trust on MultiversX
On MultiversX, the contract owner can ALWAYS deploy a contract upgrade and bypass any logic. This means #[only_owner] endpoints do NOT represent a new trust boundary — the owner already has unlimited power. Do NOT classify owner-accessible functionality as Critical or High severity. Owner-only endpoints are at most Low/Informational observations about operational convenience.
Critical (Funds at immediate risk)
ALL must be true:
Direct, exploitable path to fund loss or theft
No admin action required to exploit (owner endpoints are NOT critical — see above)
Can write a working proof-of-concept test
Impact is significant (>1% of TVL or affects all users)
Examples: Balance calculation missing reserved amounts, missing access control on withdrawals, reentrancy double-spend.
High/Major (Significant impact, exploitable)
MOST must be true:
Clear exploit path exists
Requires specific conditions but achievable
Impact affects protocol economics or user funds
Examples: Energy double-counting, gas DoS blocking critical functions, slippage attacks, admin action corrupts user-claimable state for multiple periods.
Medium (Limited impact or requires privileged access)
Issue is real but impact is contained
May require admin/owner action to trigger
Workaround exists
Examples: Admin can accidentally remove critical tokens, first N weeks blocked, storage not cleaned on upgrade (future collision risk), token removal orphans rewards for claimable window.
Low (Code quality, minor issues)
Style issues, typos, inefficient patterns with no security impact. Missing tests (unless for critical paths). Dead code that doesn't affect security.
False Positive Reduction
Before reporting ANY Critical/High finding, verify:
Exploitability: Can an attacker actually trigger this?
Proof of Concept: Can you write a failing test?
Real-world Impact: What's the actual damage?
Attack Vector: Who can exploit? (anyone / user / admin)
Existing Mitigations: Are there other checks preventing this?
Common False Positive Patterns to AVOID:
CEI violations that aren't actually exploitable. Note: MultiversX does have a real reentrancy vector via sync_call() / legacy execute_on_dest_context(...) — these synchronously re-enter the caller. Plain self.send() value transfers and register_promise() async calls cannot re-enter in the same transaction. Flag CEI violations ONLY when there is a concrete synchronous contract call between the "effects" and a subsequent state read/write on the same storage.
"Missing slippage protection" when caller provides min_amount.
Access control concerns for legitimately public functions.
"Unbounded iteration" on lists that are practically bounded by design.
Owner-level access control as a vulnerability: #[only_owner] endpoints do NOT introduce a new trust boundary. Only flag if an owner endpoint can cause unintended harm (e.g., accidentally breaking invariants), and classify as Low/Informational, not Critical/High.
Empty upgrade() as a vulnerability: Empty fn upgrade(&self) {} is the STANDARD MultiversX pattern. Do NOT flag unless there is a concrete issue (storage format change requiring migration, or storage key name collision with new mappers).
Block nonce / timestamp subtraction as underflow risk: When the stored value always originates from self.blockchain().get_block_nonce() or get_block_timestamp(), subtraction current - stored cannot underflow because the blockchain is monotonically increasing. Only flag if there is an admin setter or any code path that writes an arbitrary (potentially future) value to that storage.
Unreachable precondition findings: Before assigning severity, trace preconditions back through actual callers. If the trigger condition is unreachable from any public entry point, the finding is a false positive.
DO NOT Dismiss:
Balance calculations that miss storage locations.
Token flows where energy/amounts aren't conserved.
Admin functions that can break invariants.
Storage cleanup claims without verification.
Early period blocking without verification it's intentional.
3-5 sentences: what was audited, overall risk level, most critical findings, and key recommendation.
Risk Rating
Overall: [Safe / Low Risk / Medium Risk / High Risk / Critical]
Pattern Results (A-M)
A (Incomplete Balance): [FOUND/CLEAN]
B (Single-Period Update): [FOUND/CLEAN]
C (Unprotected Removal): [FOUND/CLEAN]
D (Removal Orphans Current): [FOUND/CLEAN]
E (Permissionless Special): [FOUND/CLEAN]
F (Storage Not Cleaned): [FOUND/CLEAN] - git history checked: [Y/N]
G (Unbounded Collection): [FOUND/CLEAN]
H (Dead Code): [FOUND/CLEAN]
I (Early Period Blocking): [FOUND/CLEAN]
J (Removal Orphans Multi-Period): [FOUND/CLEAN] - K=[periods]
K (Sync Call Reentrancy): [FOUND/CLEAN]
L (Unverified Async Returns): [FOUND/CLEAN]
M (Re-initialization): [FOUND/CLEAN]
Critical Path Verification Results
MANDATORY SECTION — Show your work:
Token Flow Analysis:
- Deposit location: [where tokens go]
- Claim location: [where tokens come from]
- Available calculation: [function name]
- Subtracts accumulated_fees: YES/NO
- Subtracts total_rewards_for_week: YES/NO
- VERDICT: CORRECT / BUG FOUND
Early Period Analysis:
- Functions with early returns: [list]
- First N periods blocked: [YES/NO, which functions]
- Intentional or bug: [assessment]
Removal Impact Analysis:
- Claim window: [N periods]
- If token removed, inaccessible data: [N periods x affected users]
Cross-Cutting Check Results
G1 (Admin Cascading): [checked] - removal impact: [N periods]
G2 (Storage Lifecycle): current=[list], removed from git=[list], cleaned=[Y/N per mapper]
G3 (Unbounded Collections): [collection]: max=[X or NONE], iterated=[Y/N]
G4 (Dead Code): add_X without usage=[list or none]
G5 (Time-Delayed): early period blocking=[list functions, intentional Y/N]
G6 (State Transitions): [transition]: source cleared=[Y/N], calcs updated=[Y/N]
G7 (Async Callbacks): [checked] - issues=[list or none]
G8 (Math Safety): [checked] - issues=[list or none]
Test Quality Score (1-10)
Unit Tests: [coverage %] - [pass/fail/skip]
Integration: [realistic mocks: Y/N]
Access Control Tests: [/10] - negative tests for admin functions
Edge Case Coverage: [/10] - time gaps, week 0, empty states
Economic Invariant Tests: [/10] - balance/energy conservation
System (Chain Sim): [available: Y/N] - [pass/fail/skip]
Build: [reproducible WASM: Y/N]
Overall Score: [1-10]
Vulnerability Matrix
| # | Title | Severity | Confidence | Category | Remediation | PoC |
|---|-------|----------|------------|----------|-------------|-----|
| 1 | ... | Critical | High | Funds at risk | [recommended fix] | Y/N |
| 2 | ... | High | Medium | DoS / Gas | [recommended fix] | Y/N |
| 3 | ... | Medium | High | Inefficiency | [recommended fix] | Y/N |
| 4 | ... | Low | High | Style | [recommended fix] | Y/N |
Finding Detail Template
For each Critical/High finding:
### Finding #[N]: [Title]
**Severity**: [Critical/High]
**Confidence**: [High/Medium/Low]
**Category**: [Balance-Calculation | Access-Control | Time-Logic | Economic | Gas-DoS | Upgrade | State-Transition | Admin-Safety | Dead-Code | Early-Period]
**Location**: [file:line]
**Root Cause**: One sentence explaining WHY this happens.
**Description**: What is the vulnerability and why it matters.
**Impact**: What an attacker can achieve. Quantify if possible (e.g., "drain X tokens").
**Proof of Concept**:
[Code or test scenario demonstrating the exploit]
**Recommendation**: How to fix it.
**Status**: [Open / Fixed / Acknowledged]
Verification Evidence
- "Ran [N] tests. [X] Passed. [Y] Skipped."
- "WASM build reproducible: [Y/N]."
- "Verified fix for Issue #N using mvx-fix-verification."
- "Variant analysis found [N] additional instances."
#[endpoint]fnupdate_config(&self, new_value: BigUint) {
// MUST CHECK:// 1. Who can call this? (access control)// 2. Input validation// 3. State transition validityself.require_caller_is_admin();
require!(new_value > 0, "Invalid value");
self.config().set(new_value);
}
Checklist for State-Changing Endpoints:
Access control implemented and correct
Input validation for all parameters
State transitions are valid
Events emitted for important changes
No DoS vectors (unbounded loops, etc.)
Category C: View Functions (Low Risk)
#[view(getBalance)]fnget_balance(&self, user: ManagedAddress) -> BigUint {
// SHOULD CHECK:// 1. Does it actually modify state? (interior mutability)// 2. Does it leak sensitive information?// 3. Is the calculation expensive (DoS via gas)?self.balances(&user).get()
}
Checklist for View Functions:
No state modification (verify no storage writes)
No sensitive data exposure
Bounded computation (no unbounded loops)
Block info usage appropriate
Category D: Init and Upgrade (Critical Risk)
#[init]fninit(&self, admin: ManagedAddress) {
// MUST CHECK:// 1. All required state initialized// 2. No way to re-initialize// 3. Admin/owner properly setself.admin().set(admin);
}
#[upgrade]fnupgrade(&self) {
// MUST CHECK:// 1. New storage mappers initialized// 2. Storage layout compatibility// 3. Migration logic correct
}
Category E: Callbacks (High Risk)
#[callback]fntransfer_callback(
&self,
#[call_result] result: ManagedAsyncCallResult<()>
) {
// MUST CHECK:// 1. Error handling (don't assume success)// 2. State reversion on failure// 3. Correct identification of original callmatch result {
ManagedAsyncCallResult::Ok(_) => {
// Success path
},
ManagedAsyncCallResult::Err(_) => {
// CRITICAL: Must handle failure!// Revert any state changes from original call
}
}
}
Verify: all checks before state changes, state changes before external calls.
Go Protocol Code
Goroutine Loop Variable Capture
grep -rn "go func" *.go
// VULNERABLEfor _, item := range items {
gofunc() {
process(item) // item may have changed!
}()
}
// SECUREfor _, item := range items {
item := item // Create local copygofunc() {
process(item)
}()
}
Map Race Conditions
grep -rn "map\[" *.go | grep -v "sync.Map"
Map Iteration Order (Determinism)
grep -rn "for.*range.*map" *.go
Map iteration in Go is random. Never use for consensus data.
Time Functions
grep -rn "time.Now()" *.go
time.Now() is forbidden in block processing — use block.Header.TimeStamp.
Appendix D: Automated Scanning with Semgrep
Custom Semgrep rules for MultiversX-specific security patterns.
Rule Structure
rules:-id:rule-identifierlanguages: [rust]
message:"Description of the issue"severity:ERROR# ERROR, WARNING, INFOpatterns:-pattern:<codepattern>metadata:category:securitytechnology:-multiversx
Unsafe Arithmetic Detection
rules:-id:mvx-unsafe-additionlanguages: [rust]
message:"Potential arithmetic overflow. Use BigUint or checked arithmetic."severity:ERRORpatterns:-pattern:$X+$Y-pattern-not:$X.checked_add($Y)-pattern-not:BigUint::from($X)+BigUint::from($Y)metadata:category:securitycwe:"CWE-190: Integer Overflow"
Floating Point Detection
rules:-id:mvx-float-forbiddenlanguages: [rust]
message:"Floating point arithmetic is non-deterministic and forbidden in smart contracts."severity:ERRORpattern-either:-pattern:"let $X: f32 = ..."-pattern:"let $X: f64 = ..."-pattern:"$X as f32"-pattern:"$X as f64"
rules:-id:mvx-unbounded-iterationlanguages: [rust]
message:"Iterating over storage mapper without bounds. Gas DoS risk."severity:ERRORpattern-either:-pattern:self.$MAPPER().iter()-pattern:|
for $ITEM in self.$MAPPER().iter() {
...
}
metadata:cwe:"CWE-400: Uncontrolled Resource Consumption"
Reentrancy Pattern
Only synchronous contract-to-contract calls can re-enter on MultiversX. Target
sync_call() and the legacy execute_on_dest_context(...) — NOT plain
self.send() / self.tx().to(...).transfer() (those are value transfers to an
account and cannot re-enter in the same transaction).