Documentation Index
Fetch the complete documentation index at: https://docs.near.org/llms.txt
Use this file to discover all available pages before exploring further.
This page provides a collection of best practices for writing smart contracts on NEAR. These practices are designed to help you write secure, efficient, and maintainable code.
Best practices
Here we lay out some best practices for writing smart contracts on NEAR, such as:
Store Account IDs efficiently
You can save on smart contract storage if using NEAR Account IDs by encoding them using base32. Since they consist of [a-z.-_] characters with a maximum length of 64 characters, they can be encoded using 5 bits per character, with terminal \0. Going to a size of 65 * 5 = 325 bits from the original (64 + 4) * 8 = 544 bits. This is a 40% reduction in storage costsEnable overflow checks
It’s usually helpful to panic on integer overflow. To enable it, add the following into your Cargo.toml file:[profile.release]
overflow-checks = true
Use require! early
Try to validate the input, context, state and access using require! before taking any actions. The earlier you panic, the more gas you will save for the caller.#[near]
impl Contract {
pub fn set_fee(&mut self, new_fee: Fee) {
require!(env::predecessor_account_id() == self.owner_id, "Owner's method");
new_fee.assert_valid();
self.internal_set_fee(new_fee);
}
}
Note: If you want debug information in the panic message or if you are using an SDK version before 4.0.0-pre.2,
the Rust assert! macro can be used instead of require!.#[near]
impl Contract {
pub fn set_fee(&mut self, new_fee: Fee) {
assert_eq!(env::predecessor_account_id(), self.owner_id, "Owner's method");
new_fee.assert_valid();
self.internal_set_fee(new_fee);
}
}
Use log!
Use logging for debugging and notifying user.When you need a formatted message, you can use the following macro:log!("Transferred {} tokens from {} to {}", amount, sender_id, receiver_id);
It’s equivalent to the following message:env::log_str(format!("Transferred {} tokens from {} to {}", amount, sender_id, receiver_id).as_ref());
Return Promise
If your method makes a cross-contract call, you probably want to return the newly created Promise.
This allows the caller (such as a near-cli or near-api-js call) to wait for the result of the promise instead of returning immediately.
Additionally, if the promise fails for some reason, returning it will let the caller know about the failure, as well as enabling NEAR Explorer and other tools to mark the whole transaction chain as failing.
This can prevent false-positives when the first or first few transactions in a chain succeed but a subsequent transaction fails.E.g.#[near]
impl Contract {
pub fn withdraw_100(&mut self, receiver_id: AccountId) -> Promise {
Promise::new(receiver_id).transfer(100)
}
}
Reuse crates from near-sdk
near-sdk re-exports the following crates:
borsh
base64
bs58
serde
serde_json
Most common crates include borsh which is needed for internal STATE serialization and
serde for external JSON serialization.When marking structs with serde::Serialize you need to use #[serde(crate = "near_sdk::serde")]
to point serde to the correct base crate./// Main contract structure serialized with Borsh
#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Contract {
pub pair: Pair,
}
/// Implements both `serde` and `borsh` serialization.
/// `serde` is typically useful when returning a struct in JSON format for a frontend.
#[near(serializers = [json, borsh])]
pub struct Pair {
pub a: u32,
pub b: u32,
}
#[near]
impl Contract {
#[init]
pub fn new(pair: Pair) -> Self {
Self {
pair,
}
}
pub fn get_pair(self) -> Pair {
self.pair
}
}
std::panic! vs env::panic
-
std::panic! panics the current thread. It uses format! internally, so it can take arguments.
SDK sets up a panic hook, which converts the generated PanicInfo from panic! into a string and uses env::panic internally to report it to Runtime.
This may provide extra debugging information such as the line number of the source code where the panic happened.
-
env::panic directly calls the host method to panic the contract.
It doesn’t provide any other extra debugging information except for the passed message.
Use workspaces
Workspaces allow you to automate workflows and run tests for multiple contracts and cross-contract calls in a sandbox or testnet environment.
Read more, workspaces-rs or workspaces-js. Here we lay out some best practices for writing smart contracts in Python on NEAR:
Validate early
Validate inputs, context, and state as early as possible in your functions. This saves gas by failing fast before executing expensive operations.from near_sdk_py import call, Context
class Contract:
def __init__(self):
self.owner_id = Context.current_account_id()
@call
def set_config(self, config_data):
# Validate permissions early
if Context.predecessor_account_id() != self.owner_id:
raise Exception("Only owner can modify config")
# Validate inputs early
if "parameter" not in config_data or not isinstance(config_data["parameter"], int):
raise Exception("Invalid config: missing or invalid parameter")
# Only proceed after validation
self._update_config(config_data)
Use proper logging
Use the Log utility for structured logging. This allows external services to parse events from your contract easily.from near_sdk_py import call, Log
@call
def transfer(self, receiver_id, amount):
# Business logic here...
# Use informational logs for regular updates
Log.info(f"Transferred {amount} tokens to {receiver_id}")
# Use structured event logging for indexable events
Log.event("transfer", {
"sender": Context.predecessor_account_id(),
"receiver": receiver_id,
"amount": amount
})
Return Promises
When making cross-contract calls, return the Promise object to let the caller track the result. This is especially important for transactions that need to be monitored.from near_sdk_py import call
from near_sdk_py.promises import Contract
@call
def withdraw(self, amount, receiver_id):
# Perform validations and business logic...
# Return the promise for better caller experience
return Contract(receiver_id).call(
"deposit",
amount=amount,
sender=Context.predecessor_account_id()
)
Handle storage efficiently
Use the SDK collections for efficient storage handling, especially for growing data sets.from near_sdk_py.collections import UnorderedMap, Vector
class TokenContract:
def __init__(self):
# Use SDK collections for efficient storage
self.tokens = Vector("t") # Efficiently stores ordered items
self.balances = UnorderedMap("b") # Efficiently stores key-value pairs
@call
def mint(self, token_id):
# Add to vector without loading all tokens
self.tokens.append(token_id)
# Update balance without loading all balances
current = self.balances.get(Context.predecessor_account_id(), 0)
self.balances[Context.predecessor_account_id()] = current + 1
Use type hints
Python’s type hints improve code readability and can help catch errors during development.from typing import Dict, List, Optional
from near_sdk_py import view, call
class Contract:
def __init__(self):
self.data: Dict[str, int] = {}
@view
def get_value(self, key: str) -> Optional[int]:
return self.data.get(key)
@call
def set_values(self, updates: Dict[str, int]) -> List[str]:
updated_keys = []
for key, value in updates.items():
self.data[key] = value
updated_keys.append(key)
return updated_keys
Follow security patterns
Apply security best practices to protect your contract from common vulnerabilities.from near_sdk_py import call, Context
from near_sdk_py.constants import ONE_NEAR
class Contract:
def __init__(self):
self.owner = Context.current_account_id()
self.minimum_deposit = ONE_NEAR // 100 # 0.01 NEAR
@call
def deposit(self):
# Check sufficient deposit to prevent spam
deposit = Context.attached_deposit()
if deposit < self.minimum_deposit:
raise Exception(f"Minimum deposit is {self.minimum_deposit}")
# Re-entrancy protection pattern
current_balance = self.balances.get(Context.predecessor_account_id(), 0)
# Update state before external calls
self.balances[Context.predecessor_account_id()] = current_balance + deposit
# Only after state update, perform any external calls
# ...
Here we lay out some best practices for writing smart contracts in Go on NEAR:
Validate early
Validate inputs, context, and state as early as possible before taking any actions. The earlier you panic, the more gas you save for the caller.// @contract:mutating
func (c *Contract) SetFee(newFee uint64) error {
caller, err := env.GetPredecessorAccountID()
if err != nil {
return err
}
// Validate permissions early
if caller != c.OwnerId {
env.PanicStr("Only the owner can set the fee")
}
// Validate input early
if newFee > 10000 {
env.PanicStr("Fee cannot exceed 10000 basis points")
}
// Only proceed after all checks pass
c.Fee = newFee
return nil
}
Use logging
Use env.LogString for debug information and to notify users of important events. Log messages are stored on-chain and visible in transaction receipts.// @contract:mutating
func (c *Contract) Transfer(to string, amount string) error {
caller, _ := env.GetPredecessorAccountID()
// Log the transfer for indexers and frontends
// Avoid "fmt" in TinyGo — use string concatenation instead
env.LogString("Transferring " + amount + " yoctoNEAR from " + caller + " to " + to)
transferAmount, err := types.U128FromString(amount)
if err != nil {
return err
}
promise.CreateBatch(to).Transfer(transferAmount)
return nil
}
Return Promises
When making cross-contract calls, use .Value() to return the promise result to the caller. This lets the caller (e.g. near-cli or near-api-js) wait for the full result and see failures in the transaction chain.// @contract:payable min_deposit=0.00001NEAR
func (c *Contract) WithdrawAndNotify(receiverId string) {
gas := uint64(10 * types.ONE_TERA_GAS)
// Return the promise chain — the caller will see the final result
promise.NewCrossContract(receiverId).
Gas(gas).
Call("on_deposit_received", map[string]string{}).
Value()
}
Use SDK collections for large data
For data sets that grow over time, use SDK collections instead of native Go slices or maps. Native collections are fully loaded into memory on every call, which costs more gas as they grow.import "github.com/vlmoon99/near-sdk-go/collections"
// Good: SDK collections load entries lazily
// @contract:state
type TokenContract struct {
Balances *collections.LookupMap[string, string] `json:"balances"`
Owners *collections.UnorderedSet[string] `json:"owners"`
}
// @contract:init
func (c *TokenContract) Init() {
c.Balances = collections.NewLookupMap[string, string]("b")
c.Owners = collections.NewUnorderedSet[string]("o")
}
Use native Go slices and maps only for small, fixed-size data (up to ~100 entries) that is always read together. For anything larger, use SDK collections.
Follow security patterns
Apply security best practices to protect your contract from common vulnerabilities.// @contract:payable min_deposit=0.01NEAR
// @contract:mutating
func (c *Contract) Deposit() error {
caller, err := env.GetPredecessorAccountID()
if err != nil {
return err
}
deposit, err := env.GetAttachedDeposit()
if err != nil {
return err
}
// Re-entrancy protection: update state BEFORE any external calls
current, _ := c.Balances.Get(caller)
currentAmount, _ := types.U128FromString(current)
newAmount, _ := currentAmount.Add(deposit)
c.Balances.Insert(caller, newAmount.String())
// Only after state is updated, perform external calls if needed
env.LogString("Deposit recorded for " + caller)
return nil
}
// Access control: only the contract itself can call this callback
// @contract:view
// @contract:promise_callback
func (c *Contract) OnExternalCallDone(result promise.PromiseResult) {
currentAccount, _ := env.GetCurrentAccountId()
caller, _ := env.GetPredecessorAccountID()
if caller != currentAccount {
env.PanicStr("Callbacks can only be called by the contract itself")
}
if !result.Success {
env.LogString("External call failed — rolling back state changes")
// manually revert any state changes made before the cross-contract call
}
}