Start now →

NEAR Smart Contract Fundamentals for EVM Developers

By Jerry George (Jbotrex) · Published April 13, 2026 · 5 min read · Source: Blockchain Tag
Ethereum

NEAR Smart Contract Fundamentals for EVM Developers

Jerry George (Jbotrex)Jerry George (Jbotrex)4 min read·Just now

--

Part 1 of a three-article series on building with NEAR as an Ethereum developer. This article covers contract structure, storage, view methods, and change methods, written for someone who already thinks in Solidity.

The Mental Model Shift

The syntax is easy. The hard part is unlearning Ethereum assumptions that feel so natural you don’t notice you’re making them.

Your First NEAR Contract

import { NearBindgen, view, call, initialize, assert, near } from "near-sdk-js";
@NearBindgen({ requireInit: true })
class Contract {
// state and storage
owner_id: string = "";
greeting: string = "";
// schema (compulsory)
static schema = {
owner_id: "string",
greeting: "string",
};
@initialize({})
init({ owner_id, greeting = "Hello from NEAR!" }: { owner_id: string; greeting?: string }): void {
this.owner_id = owner_id;
this.greeting = greeting;
}
@view({})
get_greeting(): string {
return this.greeting;
}
@call({})
set_greeting({ new_greeting }: { new_greeting: string }): void {
assert(
near.predecessorAccountId() === this.owner_id,
"Only owner can set greeting"
);
near.log(`Saving greeting ${new_greeting}`);
this.greeting = new_greeting;
}
}

This looks like a Solidity contract with decorators instead of modifiers. The execution model underneath is fundamentally different.

Contract Structure: One Account, One Contract

On Ethereum, contracts and wallets are two separate entity types. A contract address holds bytecode. An EOA holds a key. They never overlap.

NEAR has one account type. Every account can optionally hold a contract. alice.near can hold your token contract and still sign transactions with her key. The account is the contract.

@NearBindgen handles serializing your class fields to NEAR's key-value storage and deserializing them back on every call. Your class fields are your contract state. No mappings, no SSTORE, no storage slots.

@NearBindgen({ requireInit: true })
class Contract {
// state and storage
owner_id: string = ""; // persisted to account storage
greeting: string = ""; // persisted to account storage

⚠️ Always use requireInit: true. Without it, the first person to call any function triggers the constructor. In a contract that sets owner_id from near.predecessorAccountId(), that means the first caller becomes the owner. This has been exploited.

Initialization: You Control It Explicitly

On Ethereum, constructors run exactly once at deployment. NEAR doesn’t work that way. Deployment and initialization are separate steps.

@initialize({ privateFunction: true })
init({ owner_id, greeting = "Hello from NEAR!" }: { owner_id: string; greeting?: string }): void {
this.owner_id = owner_id;
this.greeting = greeting;
}
# Deploy the Wasm
near deploy mycontract.near --wasmFile contract.wasm

# Initialize with explicit owner
near call mycontract.near init \
'{"owner_id": "alice.near"}' \
--accountId alice.near

The tradeoff: this separation is more flexible than Solidity constructors. You can batch deploy and init, or delay initialization deliberately. But the contract is not in a safe state between deploy and init.

View Methods: Read-Only, Free to Call

@view({})
get_greeting(): string {
return this.greeting;
}

Property Detail Modifies state? No Costs gas? No Requires transaction? No. Direct RPC query. Solidity equivalent view modifier

near view mycontract.near get_greeting '{}'
# "Hello from NEAR!"

⚠️ Calling near.predecessorAccountId() inside a @view method will panic. There is no predecessor in a view call. Use near.currentAccountId() instead.

Change Methods: Transactions That Modify State

@call({})
set_greeting({ new_greeting }: { new_greeting: string }): void {
assert(
near.predecessorAccountId() === this.owner_id,
"Only owner can set greeting"
);
near.log(`Saving greeting ${new_greeting}`);
this.greeting = new_greeting;
}

NEAR Solidity Equivalent @call non-view function near.predecessorAccountId() msg.sender assert require

assert panics and reverts state changes, but only for the current receipt. In cross-contract call chains, each call is a separate receipt. A failure in one does not automatically unwind previous receipts. For a single-contract method, it behaves exactly like require.

# Owner sets greeting
near call mycontract.near set_greeting \
'{"new_greeting": "Hello World"}' \
--accountId alice.near

# Non-owner attempt, panics: "Only owner can set greeting"
near call mycontract.near set_greeting \
'{"new_greeting": "Hacked"}' \
--accountId bob.near

A Note on near-sdk-js Versions

You will likely encounter patterns in learning environments that differ from the official documentation:

@NearBindgen({})
class Contract {
constructor({ owners } = { owners: [] }) {
this.owners = owners || [];
}
@call({})
init({ initial_owner }) { // plain @call, not @initialize
this.owners.push(initial_owner);
}
@call({})
add_owner({ account }) {
require(this.is_owner(near.predecessorAccountId()), "Only owner");
this.owners.push(account);
}
}

Learning Env Production Init @call named init @initialize decorator Uninit guard None requireInit: true Access control require() assert() Collections Plain arrays/objects Vector, LookupMap Constructor Inline predecessorAccountId() Empty strings + init

Three of these differences are safety issues:

The State Model: What Actually Gets Stored

In Solidity, every state variable maps to a specific storage slot. NEAR uses a key-value store. @NearBindgen serializes your entire class via Borsh and deserializes it back on every call.

⚠️ Schema changes require a migration function. If you redeploy with a different class structure (adding a field, renaming one, changing a type), the old serialized bytes won’t deserialize into your new struct. The contract panics on every call.

Storage on NEAR is also not free. Every byte requires a proportional amount of NEAR tokens locked in the account. This is called storage staking. Tokens are returned when storage is freed. It’s not a fee; it’s a deposit.

The Five Core Patterns

Every NEAR contract is built on these five patterns:

Get these right and you have the skeleton of any NEAR contract. Everything else (collections, events, cross-contract calls, upgrades) is built on top.

This article was originally published on Blockchain Tag and is republished here under RSS syndication for informational purposes. All rights and intellectual property remain with the original author. If you are the author and wish to have this article removed, please contact us at [email protected].

NexaPay — Accept Card Payments, Receive Crypto

No KYC · Instant Settlement · Visa, Mastercard, Apple Pay, Google Pay

Get Started →