Deploying Smart Contracts on Near: Rust, Accounts, and Storage
When a DeFi startup team decided to migrate its liquidity protocol to Near, the first contract consumed 15 NEAR on storage due to incorrect data structures — 15,000 user balance entries were stored in a Vector when they needed a LookupMap. That's when we faced the core difference between Near and EVM: here, the developer pays for storage. Our 5+ years of experience in the Near ecosystem helps avoid such mistakes and save up to 40% on storage costs with proper design. Let’s break down how to do it right so you don’t waste deposits.
How Near’s Account Model Works
In Near, each smart contract lives on a named account (myapp.near, myapp.testnet). The account stores the contract code and its state. The storage cost is 1 NEAR per 100 KB of state, and these funds are locked on the account balance. This is called storage staking.
Practical consequence: if the contract stores user data, you need storage_deposit logic — the user deposits NEAR before registration, and those funds cover their storage. The NEP-145 standard defines this pattern for fungible tokens.
#[payable]
pub fn storage_deposit(&mut self, account_id: Option<AccountId>) -> StorageBalance {
let amount = env::attached_deposit();
let account_id = account_id.unwrap_or_else(env::predecessor_account_id);
// minimum 0.00125 NEAR per account record
assert!(amount >= STORAGE_PER_ACCOUNT, "Insufficient deposit");
// ...
}Sub-accounts are a standard pattern for factory contracts: token.myapp.near, pool.myapp.near. Each sub-account is a fully independent account.
Why Storage Staking Is Critical for Your Budget
The choice of data structure directly impacts costs. Here’s a comparison of popular collections:
| Collection | Iteration | Storage cost per 1000 records | Gas per write |
|---|---|---|---|
LookupMap |
No | ~0.01 NEAR | 2 TGas |
UnorderedMap |
Yes | ~0.015 NEAR | 3 TGas |
Vector |
Yes | ~0.02 NEAR | 4 TGas |
Rust contracts on Near are 2-3 times more performance-efficient than JavaScript for the same logic — our tests on identical operations (1000 records, ~3 TGas vs ~8 TGas) confirm this.
Runtime and Languages
The Near VM executes WASM bytecode. Two languages are officially supported.
Rust + near-sdk-rs — the primary choice for production. The SDK provides macros like #[near_bindgen], #[init], BorshSerialize/BorshDeserialize for state serialization. Borsh (Binary Object Representation Serializer for Hashing) is a deterministic binary format mandatory for storing Near contract state.
JavaScript/TypeScript + near-sdk-js — for prototypes and simple logic. It compiles to WASM via QuickJS, with noticeably lower performance and tighter gas limits.
Example of a minimal Rust contract:
use near_sdk::{near_bindgen, env, AccountId};
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Counter {
value: i64,
}
#[near_bindgen]
impl Counter {
#[init]
pub fn new() -> Self {
Self {
value: 0,
}
}
pub fn increment(&mut self) {
self.value += 1;
}
pub fn get(&self) -> i64 {
self.value
}
} Cross-Contract Calls and Async Model
Near is an asynchronous sharded blockchain. A cross-contract call is not executed synchronously within the transaction — it creates a separate receipt that is processed in the next block. The result comes back in a callback. This model complicates design but allows error handling without full state rollback.
#[near_bindgen]
impl MyContract {
pub fn call_external(&mut self, contract_id: AccountId) -> Promise {
ext_other_contract::ext(contract_id)
.with_static_gas(Gas(5 * TGAS))
.get_value()
.then(
Self::ext(env::current_account_id())
.with_static_gas(Gas(5 * TGAS))
.on_value_received()
)
}
#[private]
pub fn on_value_received(&mut self, #[callback_result] result: Result<u64, PromiseError>) {
match result {
Ok(value) => {
/* processing */
}
Err(_) => {
env::panic_str("External call failed")
}
}
}
}#[private] — a macro that adds the check assert_eq!(env::current_account_id(), env::predecessor_account_id()). Without it, anyone can call the callback directly.
Build and Deploy
Tooling: cargo-near — official CLI for building, near-cli-rs (Rust-rewritten version) for deployment.
# Installation
cargo install cargo-near
cargo install near-cli-rs
# Build optimized WASM
cargo near build
# Deploy to testnet
near contract deploy myapp.testnet \
use-file ./target/near/myapp.wasm \
without-init-call \
network-config testnet \
sign-with-keychain \
send
The WASM file after building with cargo-near automatically passes through wasm-opt (Binaryen) — this reduces size by up to 30% and saves on storage.
For mainnet deployment, use a multisig account or near-cli with a hardware wallet (--useLedgerKey). Deploying without initialization leaves the contract uninitialized; the first call to new() sets the initial state.
Testing
Unit tests — standard Rust unit tests with VMContextBuilder to mock the environment (predecessor, attached_deposit, block_timestamp).
Workspaces-rs — integration tests that run a local Near sandbox and deploy real WASM. This is the de facto standard for testing cross-contract interactions:
#[tokio::test]
async fn test_full_flow() -> anyhow::Result<()> {
let worker = near_workspaces::sandbox().await?;
let wasm = std::fs::read("./target/near/myapp.wasm")?;
let contract = worker.dev_deploy(&wasm).await?;
let result = contract.call("increment")
.transact().await?;
assert!(result.is_success());
Ok(())
} Typical Mistakes When Migrating from EVM
- No
msg.senderas an address. In Near,env::predecessor_account_id()is a string, not 20 bytes. Comparison uses==forAccountId. - Panic instead of revert.
env::panic_str("message")orassert!()— similar torequire()in Solidity. Gas for panic is not refunded. - Iteration over
LookupMap.LookupMapis not iterable. For iterable collections, useUnorderedMaporVector. Choosing the right data structure affects gas. - Gas units. 1 TGas = 10¹² gas. A simple call ~2-3 TGas, a cross-contract call +5 TGas minimum. Transaction limit is 300 TGas.
More about sub-accounts
Sub-accounts are created by deploying a contract to a name like `subdomain.main.near`. They inherit permissions from the parent account but have their own storage. For example, `token.mybank.near` is an independent contract with its own balance, not linked to `mybank.near`. This is convenient for multi-token platforms.What's Included in Turnkey Development
- Requirements audit and language selection (Rust/JS).
- Storage model design — critical for cost (correct collection types reduce storage by 30-50%).
- Development with unit tests.
- Integration testing via workspaces.
- Deployment to testnet, verification via Near Explorer.
- Deployment to mainnet with multisig.
- Handover of contract access and documentation.
- Post-deployment support — 1 month included.
Deployment time for ready-to-go code — 4 hours. Contract development from scratch with testing: 1-5 days depending on complexity.
We guarantee compliance with Near standards (NEP-145, NEP-141) and provide full documentation. Our engineers are Near Certified Developers.
Order turnkey development — we will evaluate your project within 1 business day. Contact us for a project assessment — we will analyze requirements, propose the optimal approach, and meet deadlines without surprises.







