multiversx-entry-points
MultiversX Entry Point Analyzer
Identify the complete attack surface of a MultiversX smart contract by enumerating all public interaction points and classifying their risk levels. This is typically the first step in any security review.
When to Use
- Starting a new security audit
- Documenting contract public interface
- Assessing access control coverage
- Mapping data flow through the contract
- Identifying high-risk endpoints for focused review
1. Entry Point Identification
MultiversX Macros That Expose Functions
| Macro | Visibility | Risk Level | Description |
|---|---|---|---|
#[endpoint] |
Public write | High | State-changing public function |
#[view] |
Public read | Low | Read-only public function |
#[payable("*")] |
Accepts any token | Critical | Handles value transfers |
#[payable("EGLD")] |
Accepts EGLD only | Critical | Handles native currency |
#[init] |
Deploy only | Medium | Constructor (runs once) |
#[upgrade] |
Upgrade only | Critical | Migration logic |
#[callback] |
Internal | High | Async call response handler |
#[only_owner] |
Owner restricted | Medium | Admin functions |
Scanning Commands
# Find all endpoints
grep -n "#\[endpoint" src/*.rs
# Find all payable endpoints
grep -n "#\[payable" src/*.rs
# Find all views
grep -n "#\[view" src/*.rs
# Find callbacks
grep -n "#\[callback" src/*.rs
# Find init and upgrade
grep -n "#\[init\]\|#\[upgrade\]" src/*.rs
2. Risk Classification
Category A: Payable Endpoints (Critical Risk)
Functions receiving value require the most scrutiny.
#[payable("*")]
#[endpoint]
fn deposit(&self) {
// MUST CHECK:
// 1. Token identifier validation
// 2. Amount > 0 validation
// 3. Correct handling of multi-token transfers
// 4. State updates before external calls
let payment = self.call_value().single_esdt();
require!(
payment.token_identifier == self.accepted_token().get(),
"Wrong token"
);
require!(payment.amount > 0, "Zero amount");
// Process deposit...
}
Checklist for Payable Endpoints:
- Token ID validated against expected token(s)
- Amount checked for minimum/maximum bounds
- Multi-transfer handling if
all_esdt_transfers()used - Nonce validation for NFT/SFT
- Reentrancy protection (Checks-Effects-Interactions)
Category B: Non-Payable State-Changing Endpoints (High Risk)
Functions that modify state without payment.
#[endpoint]
fn update_config(&self, new_value: BigUint) {
// MUST CHECK:
// 1. Who can call this? (access control)
// 2. Input validation
// 3. State transition validity
self.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)
Read-only functions, but still need review.
#[view(getBalance)]
fn get_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 (
get_block_timestamp()may differ off-chain)
Category D: Init and Upgrade (Critical Risk)
Lifecycle functions with special considerations.
#[init]
fn init(&self, admin: ManagedAddress) {
// MUST CHECK:
// 1. All required state initialized
// 2. No way to re-initialize
// 3. Admin/owner properly set
self.admin().set(admin);
}
#[upgrade]
fn upgrade(&self) {
// MUST CHECK:
// 1. New storage mappers initialized
// 2. Storage layout compatibility
// 3. Migration logic correct
}
Category E: Callbacks (High Risk)
Async call handlers with specific vulnerabilities.
#[callback]
fn transfer_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 call
match result {
ManagedAsyncCallResult::Ok(_) => {
// Success path
},
ManagedAsyncCallResult::Err(_) => {
// CRITICAL: Must handle failure!
// Revert any state changes from original call
}
}
}
3. Analysis Workflow
Step 1: List All Entry Points
Create an inventory table:
| Endpoint | Type | Payable | Access | Storage Touched | Risk |
|----------|------|---------|--------|-----------------|------|
| deposit | endpoint | * | Public | balances | Critical |
| withdraw | endpoint | No | Public | balances | Critical |
| setAdmin | endpoint | No | Owner | admin | High |
| getBalance | view | No | Public | balances (read) | Low |
| init | init | No | Deploy | admin, config | Medium |
Step 2: Tag Access Control
For each endpoint, document who can call it:
// Public - anyone can call
#[endpoint]
fn public_function(&self) { }
// Owner only - blockchain owner
#[only_owner]
#[endpoint]
fn owner_function(&self) { }
// Admin only - custom access control
#[endpoint]
fn admin_function(&self) {
self.require_caller_is_admin();
}
// Whitelisted - address in set
#[endpoint]
fn whitelist_function(&self) {
let caller = self.blockchain().get_caller();
require!(self.whitelist().contains(&caller), "Not whitelisted");
}
Step 3: Tag Value Handling
Classify how each endpoint handles value:
| Tag | Meaning | Example |
|---|---|---|
| Refusable | Rejects payments | Default (no #[payable]) |
| EGLD Only | Accepts EGLD | #[payable("EGLD")] |
| Token Only | Specific ESDT | #[payable("TOKEN-abc123")] |
| Any Token | Any payment | #[payable("*")] |
| Multi-Token | Multiple payments | Uses all_esdt_transfers() |
Step 4: Graph Data Flow
Map which storage mappers each endpoint reads/writes:
deposit() ──writes──▶ balances
──writes──▶ total_deposited
──reads───▶ accepted_token
withdraw() ──reads/writes──▶ balances
──reads────────▶ withdrawal_fee
getBalance() ──reads──▶ balances
4. Specific Attack Vectors
Privilege Escalation
Is a sensitive endpoint accidentally public?
// VULNERABLE: Missing access control
#[endpoint]
fn set_admin(&self, new_admin: ManagedAddress) {
self.admin().set(new_admin); // Anyone can become admin!
}
// CORRECT: Protected
#[only_owner]
#[endpoint]
fn set_admin(&self, new_admin: ManagedAddress) {
self.admin().set(new_admin);
}
DoS via Unbounded Growth
Can public endpoints cause unbounded storage growth?
// VULNERABLE: Public endpoint adds to unbounded set
#[endpoint]
fn register(&self) {
let caller = self.blockchain().get_caller();
self.participants().insert(caller); // Grows forever!
}
// Attack: Call register() with many addresses until
// any function iterating participants() runs out of gas
Missing Payment Validation
Does a payable endpoint verify what it receives?
// VULNERABLE: Accepts any token
#[payable("*")]
#[endpoint]
fn stake(&self) {
let payment = self.call_value().single_esdt();
self.staked().update(|s| *s += payment.amount); // Fake tokens accepted!
}
Callback State Assumptions
Does a callback assume the async call succeeded?
// VULNERABLE: Assumes success
#[callback]
fn on_transfer_complete(&self) {
// This runs even if transfer FAILED!
self.transfer_count().update(|c| *c += 1);
}
5. Output Template
# Entry Point Analysis: [Contract Name]
## Summary
- Total Endpoints: X
- Payable Endpoints: Y (Critical)
- State-Changing: Z (High)
- Views: W (Low)
## Detailed Inventory
### Critical Risk (Payable)
| Endpoint | Accepts | Access | Concerns |
|----------|---------|--------|----------|
| deposit | * | Public | Token validation needed |
### High Risk (State-Changing)
| Endpoint | Access | Storage Modified | Concerns |
|----------|--------|------------------|----------|
| withdraw | Public | balances | Amount validation |
### Medium Risk (Admin)
| Endpoint | Access | Storage Modified | Concerns |
|----------|--------|------------------|----------|
| setConfig | Owner | config | Privilege escalation if misconfigured |
### Low Risk (Views)
| Endpoint | Storage Read | Concerns |
|----------|--------------|----------|
| getBalance | balances | None |
## Access Control Matrix
| Endpoint | Public | Owner | Admin | Whitelist |
|----------|--------|-------|-------|-----------|
| deposit | Yes | - | - | - |
| setAdmin | - | Yes | - | - |
## Recommended Focus Areas
1. [Highest priority endpoint and why]
2. [Second priority]
3. [Third priority]