Skip to main content

Ethereum to NEAR Migration Guide

A comprehensive guide for Ethereum developers migrating to NEAR Protocol - covering concepts, code patterns, and practical migration steps.

Why Migrate to NEAR?​

  • Human-readable accounts: alice.near instead of 0x7a3d...
  • Predictable gas costs: No fee spikes during congestion
  • Built-in account abstraction: Access keys with granular permissions
  • Horizontal scaling: Sharded architecture grows with demand
  • Developer flexibility: Build in Rust or JavaScript

Conceptual Differences​

Account Model​

EthereumNEAR
20-byte address (0x...)Named accounts (alice.near)
EOA vs Contract accountsAll accounts can hold code + data
Single private key = full controlMultiple access keys with permissions
No sub-accountsSub-accounts (app.alice.near)

Key Insight: NEAR accounts are like domain names. You own alice.near and can create app.alice.near, nft.alice.near, etc.

Access Keys​

NEAR's killer feature for UX:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Named Account (alice.near) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ FullAccess β”‚ β”‚ FunctionCallβ”‚ β”‚ FunctionCallβ”‚ β”‚
β”‚ β”‚ Key (owner) β”‚ β”‚ Key (dApp) β”‚ β”‚ Key (game) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ Only: swap()β”‚ β”‚ Only: play()β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • FullAccess keys: Like Ethereum private keys - full control
  • FunctionCall keys: Limited to specific contract methods, with allowance

Gas Model​

EthereumNEAR
Gas auction (higher bid = faster)Fixed gas price
Fees spike during congestionPredictable costs
User always paysContract can pay (prepaid gas)
ETH for gasNEAR for gas

NEAR Gas Costs (approximate):

  • Simple transfer: ~0.00045 NEAR
  • Contract call: ~0.0005-0.001 NEAR
  • Contract deploy: ~0.05-0.5 NEAR

Storage Model​

EthereumNEAR
Pay gas to writeStake NEAR to reserve storage
Free readsPaid reads AND writes
256-bit storage slotsKey-value with serialization

Storage Staking: 1 NEAR β‰ˆ 10KB of storage. When you delete data, you get the stake back.


Code Translation​

Basic Mappings​

SolidityNEAR RustNEAR JS
msg.senderenv::predecessor_account_id()near.predecessorAccountId()
tx.originenv::signer_account_id()near.signerAccountId()
address(this)env::current_account_id()near.currentAccountId()
msg.valueenv::attached_deposit()near.attachedDeposit()
block.timestampenv::block_timestamp()near.blockTimestamp()
require(cond, msg)require!(cond, msg)assert(cond, msg)

ERC-20 β†’ NEP-141 Token​

Solidity (ERC-20):

function transfer(address to, uint256 amount) public returns (bool) {
require(_balances[msg.sender] >= amount, "Insufficient balance");
_balances[msg.sender] -= amount;
_balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}

NEAR Rust (NEP-141):

#[payable]
pub fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option<String>) {
require!(env::attached_deposit() >= 1, "Requires 1 yoctoNEAR");

let sender_id = env::predecessor_account_id();
let amount: u128 = amount.into();

self.internal_transfer(&sender_id, &receiver_id, amount, memo);
}

Storage Patterns​

SolidityNEAR Rust
mapping(address => uint)LookupMap<AccountId, u128>
mapping(address => mapping(...))Nested LookupMap with prefix
uint[] arrayVector<T>
address[] with iterationUnorderedSet<AccountId>

Cross-Contract Calls​

Critical Difference: NEAR cross-contract calls are ASYNCHRONOUS.

Solidity (synchronous):

uint balance = IERC20(token).balanceOf(address(this));
// Balance available immediately

NEAR Rust (async with callback):

// Initiate call - returns Promise, not result
ext_token::ext(token_account)
.ft_balance_of(env::current_account_id())
.then(
Self::ext(env::current_account_id())
.on_balance_received()
)

// Handle result in callback
#[private]
pub fn on_balance_received(&self, #[callback_result] result: Result<U128, PromiseError>) -> U128 {
match result {
Ok(balance) => balance,
Err(_) => U128(0),
}
}

Migration Checklist​

1. Environment Setup​

# Install NEAR CLI
npm install -g near-cli-rs

# Install Rust (for contracts)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknown

# Create testnet account
near account create-account fund-myself myapp.testnet

2. Contract Migration Steps​

  1. Audit Solidity contracts - Identify all state variables, external calls, events
  2. Map to NEAR patterns - Use the translation tables above
  3. Handle async calls - Redesign any synchronous external calls
  4. Implement storage - Choose appropriate collections (LookupMap vs UnorderedMap)
  5. Add NEP-297 events - Replace Solidity events with standard logging
  6. Test thoroughly - Use near-workspaces for integration tests

3. Frontend Migration​

Before (ethers.js):

const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const contract = new ethers.Contract(address, abi, signer);
await contract.transfer(to, amount);

After (NEAR wallet-selector):

import { setupWalletSelector } from '@near-wallet-selector/core';

const selector = await setupWalletSelector({ network: 'mainnet', modules: [...] });
const wallet = await selector.wallet();

await wallet.signAndSendTransaction({
receiverId: 'token.near',
actions: [{
type: 'FunctionCall',
params: { methodName: 'ft_transfer', args: { receiver_id: to, amount }, gas: '30000000000000', deposit: '1' }
}]
});

Common Gotchas​

1. Async Cross-Contract Calls​

Your Solidity mental model won't work. Design state machines that handle callbacks.

2. Storage Costs​

Unlike Ethereum's "pay once" model, NEAR requires ongoing storage staking. Optimize data structures.

3. 1 yoctoNEAR Security Deposit​

NEP-141 transfers require 1 yoctoNEAR attached to prevent exploits. Don't forget it!

4. Access Key Management​

Leverage function-call keys for better UX. Users can approve limited permissions without full wallet access.

5. Account Creation Costs​

Creating accounts costs NEAR (for storage). Budget for this in your dApp economics.


Aurora: The Fast Path​

If you need to migrate quickly, Aurora runs a full EVM on NEAR:

  • Deploy Solidity contracts unchanged
  • Use existing Ethereum tooling (Hardhat, Foundry)
  • Bridge assets from Ethereum
  • Slightly higher fees than native NEAR

Best for: Quick migrations, complex Solidity codebases, teams without Rust experience.


Resources​