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.







