Skip to main content
Security is critical when building smart contracts. This guide covers essential security concepts and best practices to keep your dApp safe.
Smart contracts handle real money!Always prioritize security in your development process. A vulnerability can lead to significant financial losses.

Security Checklist

Before deploying your contract, review this checklist:

Anatomy & Functions

Environment & Context

Storage & State

Actions & Transfers

Cross-Contract Calls


Common Vulnerabilities

1. Callback Security

Cross-contract calls are asynchronous. The contract state can change between the call and callback.
#[near]
impl Contract {
    #[payable]
    pub fn buy_item(&mut self, item_id: u64) -> Promise {
        let buyer = env::predecessor_account_id();
        let price = self.get_item_price(item_id);
        let deposit = env::attached_deposit();
        
        assert!(deposit >= price, "Insufficient deposit");
        
        // ❌ VULNERABLE: Item marked as sold immediately
        self.mark_as_sold(item_id, buyer.clone());
        
        // External call to transfer NFT
        ext_nft::ext(self.nft_contract.clone())
            .transfer(buyer.clone(), item_id)
            .then(
                Self::ext(env::current_account_id())
                    .buy_callback(item_id, buyer)
            )
    }

    #[private]
    pub fn buy_callback(&mut self, item_id: u64, buyer: AccountId) {
        // If NFT transfer fails, item is still marked as sold!
    }
}
Key Points:
  • Don’t finalize state changes before callbacks execute
  • Always check callback results
  • Refund users if external calls fail
  • Mark callbacks as private

2. Reentrancy Attacks

Although less common in NEAR than other blockchains, reentrancy is still possible through cross-contract calls.
#[near]
impl Contract {
    pub fn withdraw(&mut self) -> Promise {
        let caller = env::predecessor_account_id();
        let amount = self.balances.get(&caller).unwrap_or(0);
        
        assert!(amount > 0, "No balance");
        
        // ❌ VULNERABLE: Balance cleared after transfer
        Promise::new(caller.clone())
            .transfer(NearToken::from_yoctonear(amount))
            .then(
                Self::ext(env::current_account_id())
                    .withdraw_callback(caller)
            )
    }

    #[private]
    pub fn withdraw_callback(&mut self, caller: AccountId) {
        // Balance cleared here - too late!
        self.balances.insert(caller, 0);
    }
}
Best Practice: Always update state before making external calls or transfers (“checks-effects-interactions” pattern).

3. Front-Running

Transactions are visible before execution. Attackers can see pending transactions and submit their own with higher gas. Vulnerable Scenarios:
  • DEX trades without slippage protection
  • Auctions without commit-reveal schemes
  • First-come-first-served mechanisms
#[payable]
pub fn buy_token(&mut self) {
    let amount = env::attached_deposit();
    // ❌ Uses current price - can be front-run
    let tokens = amount / self.token_price;
    self.mint_tokens(env::predecessor_account_id(), tokens);
}

4. Storage Staking Attacks

Attackers can drain contract balance by forcing it to pay for storage.
pub fn register_user(&mut self, user_id: AccountId) {
    // ❌ Anyone can register any user, draining contract balance
    self.users.insert(user_id, UserData::default());
}

5. Access Control

Always validate who is calling your functions.
pub fn admin_withdraw(&mut self, amount: Balance) {
    // ❌ No access control - anyone can withdraw!
    Promise::new(env::predecessor_account_id())
        .transfer(NearToken::from_yoctonear(amount));
}
Common Mistake: Using signer_account_id instead of predecessor_account_id.
  • predecessor_account_id - immediate caller (use for access control)
  • signer_account_id - original transaction signer (can be different in cross-contract calls)

6. Random Number Generation

Generating secure random numbers on-chain is difficult.
// ❌ INSECURE: Predictable randomness
pub fn get_random(&self) -> u64 {
    env::block_timestamp() % 100
}

7. Integer Overflow/Underflow

Always check arithmetic operations.
// In Cargo.toml: [profile.release] (missing overflow-checks)

pub fn transfer(&mut self, to: AccountId, amount: u128) {
    let sender = env::predecessor_account_id();
    let sender_balance = self.balances.get(&sender).unwrap_or(0);
    
    // ❌ Can underflow in release mode!
    self.balances.insert(sender, sender_balance - amount);
    
    let receiver_balance = self.balances.get(&to).unwrap_or(0);
    // ❌ Can overflow!
    self.balances.insert(to, receiver_balance + amount);
}

Security Best Practices

Always structure your functions as:
  1. Checks - Validate inputs and conditions
  2. Effects - Update state
  3. Interactions - Call external contracts
This prevents reentrancy and ensures state consistency.
fn require_owner(&self) {
    assert_eq!(
        env::predecessor_account_id(),
        self.owner,
        "Only owner"
    );
}

pub fn admin_function(&mut self) {
    self.require_owner();
    // Function logic
}
Never trust user inputs:
pub fn set_price(&mut self, price: u128) {
    assert!(price > 0, "Price must be positive");
    assert!(price <= MAX_PRICE, "Price too high");
    self.price = price;
}
Always ensure sufficient balance for storage:
let initial_storage = env::storage_usage();
// Perform operation
let final_storage = env::storage_usage();
let storage_cost = (final_storage - initial_storage) as u128 
                    * env::storage_byte_cost();
assert!(env::account_balance() >= storage_cost, "Insufficient balance");
  • Write unit tests for all functions
  • Write integration tests for realistic scenarios
  • Test edge cases and failure modes
  • Test on testnet before mainnet
Follow NEAR standards:
  • NEP-141 for Fungible Tokens
  • NEP-171 for NFTs
  • NEP-145 for Storage Management
These standards have been battle-tested and audited.

Security Auditing

1

Self-Review

Go through the security checklist and review your code for common vulnerabilities.
2

Peer Review

Have other developers review your code, especially security-critical sections.
3

Testnet Testing

Deploy to testnet and test extensively with real users before mainnet.
4

Professional Audit

For contracts handling significant value, get a professional security audit.
5

Bug Bounty

Consider running a bug bounty program. NEAR has a bug bounty program.

Resources

Bug Bounty Program

Report security vulnerabilities and earn rewards

Testing Guide

Learn how to test your contracts

NEAR Standards

Follow established contract standards

Example Contracts

Study secure example implementations

Next Steps

Testing

Learn comprehensive testing strategies

Deploy

Deploy your secure contract

Upgrade

Learn safe upgrade practices

Monitor

Monitor your deployed contract

Build docs developers (and LLMs) love