Skip to main content
Tips to write better NEAR smart contracts.

Code Organization

Use Clear Naming

// Good
pub fn transfer_tokens(&mut self, recipient: AccountId, amount: u128) { ... }

// Bad
pub fn tt(&mut self, r: AccountId, a: u128) { ... }
#[near]
impl MyContract {
    // === View Methods ===
    pub fn get_balance(&self) -> u128 { ... }
    pub fn get_owner(&self) -> AccountId { ... }

    // === Mutating Methods ===
    pub fn transfer(&mut self, ...) { ... }
    pub fn withdraw(&mut self, ...) { ... }

    // === Admin Methods ===
    pub fn set_owner(&mut self, ...) { ... }
}

Keep Methods Small

Each method should do one thing. If a method is getting long, break it into smaller private functions.

Gas Optimization

Minimize Storage Writes

Storage operations are expensive. Batch updates when possible.
// Good - one write
self.balances.insert(&account, &new_balance);

// Bad - multiple writes in a loop
for item in items {
    self.data.insert(&item.id, &item);
}

Use Efficient Data Structures

Use CaseRecommended
Key-value lookupLookupMap
Iterable collectionVector
Unique setLookupSet
Small fixed dataLazyOption

Avoid Large Return Values

Returning large amounts of data costs gas. Use pagination for lists.
// Good - paginated
pub fn get_items(&self, from: u64, limit: u64) -> Vec<Item> {
    self.items.iter().skip(from).take(limit).collect()
}

// Bad - returns everything
pub fn get_all_items(&self) -> Vec<Item> {
    self.items.to_vec()
}

Security

Validate Inputs

Always check user inputs before processing.
pub fn transfer(&mut self, amount: u128) {
    require!(amount > 0, "Amount must be positive");
    require!(amount <= self.balance, "Insufficient balance");
    // ...
}

Check Caller Identity

For admin functions, verify the caller.
pub fn admin_action(&mut self) {
    require!(
        env::predecessor_account_id() == self.owner,
        "Only owner can call this"
    );
    // ...
}

Handle Errors Gracefully

Use require! for expected failures, panic! for unexpected ones.
// Expected failure - user error
require!(self.balances.contains_key(&account), "Account not found");

// Unexpected failure - bug
let value = self.data.get(&key).expect("Data should exist");

Be Careful with Cross-Contract Calls

Cross-contract calls can fail. Always handle the callback.
#[private]
pub fn on_transfer_complete(&mut self, #[callback_result] result: Result<(), PromiseError>) {
    match result {
        Ok(_) => { /* success */ }
        Err(_) => { /* rollback changes */ }
    }
}

Testing

Test Happy Path and Edge Cases

#[test]
fn test_transfer_success() { ... }

#[test]
fn test_transfer_zero_fails() { ... }

#[test]
fn test_transfer_insufficient_balance() { ... }

Use Descriptive Test Names

// Good
#[test]
fn test_withdraw_fails_when_balance_is_zero() { ... }

// Bad
#[test]
fn test_withdraw() { ... }

Quick Reference

Do

  • Validate all inputs
  • Use efficient data structures
  • Write tests
  • Keep methods focused

Don't

  • Store unnecessary data
  • Return large datasets
  • Skip error handling
  • Trust user input blindly