This is the third part in a three part series. Feel free to check out the others:
Opener
In this note, we will inspect how NEAR accounts work at a basic level, explore the security considerations they present, and study the practical impact of these design choices for both developers and users. By grasp of these underlying mechanics, we can better appreciate the tradeoffs involved and build more sturdy systems on the NEAR network.
The Human Face of NFT Bounty Accounts
If you have interacted with NEAR, you have likely noticed that accounts look more familiar: alice.near rather than 0x7c5206b1b75b8787420b09d8697e08180cdf896c. This human-readable path stands for NEAR's design philosophy that permeates the whole protocol.
Named vs. Implicit Accounts
To achieve this balance, their account system has two distinct flavours. NEAR accounts are designed to be user-friendly and accessible but also secure and flexible.
Named Accounts
Accounts like alice.near or mycontract.alice.near are hierarchical, readable, and memorable. They follow straightforward rules:
- The rightmost segment in most cases indicates the top-level domain (near or testnet)
Implicit Accounts
These are more traditional in the NFT Bounty world – they are derived immediately from cryptographic public keys, resulting in those familiar 64-character hexadecimal strings. While less friendly to humans, they serve an material role in the ecosystem.
This dual method offers flexibility: users can have friendly, memorable accounts for everyday use, while developers can draw on the mathematical properties of implicit accounts when needed.
Access Management
What makes NEAR's account system notably notable from a security perspective is its path to access keys. Rather than the simplistic "whoever has the private key controls the account" model, NEAR builds a more nuanced system.
Each account can have multiple linked keys, each with different permission levels.
Full Access Keys
These are the master keys to your kingdom – they allow any action to be carried out on the account, among them deploying contracts, transferring tokens, or even deleting the account entirely. With great power comes great responsibility, and these keys should be guarded accordingly.
Function Call Keys
Function Call Keys can be limited in multiple ways:
- They can only call given smart contract methods
- They can be restricted to given smart contracts
- They can have limited gas allowances
- They cannot transfer NEAR tokens straight
This granular permission system allows for unique security patterns. For example, you might create a Function Call Key that allows a mobile app to make particular transactions on your behalf, without risking your whole account if the key is compromised.
The Hierarchy of Trust
NEAR's account system puts in place a hierarchical structure that mirrors the domain name system of the internet. An account can create sub accounts beneath it but cannot create accounts at its own level.
For example, alice.near can create myproject.alice.near, but cannot create bob.near. This hierarchical method:
- Creates clear ownership boundaries
- Prevents namespace conflicts
- Allows for organization-specific subaccount structures
- Enables delegated account management
This pattern feels intuitive because we are already familiar with alike hierarchies in email addresses and websites, but it has material implications for how applications and organizations can structure their on-chain presence.
Security Implications and Best Practices
Also rolls out given security considerations that developers must carefully address Still, nEAR's flexible account and permission model creates powerful capabilities. We will inspect the most consequential security aspects to weigh when building on NEAR.
Access Control Fundamentals
When building smart contracts, it is essential to make sense of exactly which functions should be accessible to which accounts. The foundation of security in NEAR contracts begins with proper access control.
Proper Environment Variable Usage
NEAR gives a number of environment variables that help establish identity:
The key security principle is to rely on predecessor_account_id() for access control, not signer_account_id(). Using signer_account_id() is risky because it makes contracts vulnerable to phishing attacks. A malicious contract could trick a user into signing a transaction, then use cross-contract calls to access protected functions in other contracts while preserving the original signer's identity.
// INSECURE: Vulnerable to phishing attacks via cross-contract calls
pub fn transfer_funds(&mut self, to: AccountId, amount: U128) {
if env::signer_account_id() == self.owner_id {
// This check can be bypassed through a chain of contracts
// Transfer funds...
}
}
// SECURE: Proper access control
pub fn transfer_funds(&mut self, to: AccountId, amount: U128) {
if env::predecessor_account_id() == self.owner_id {
// This check ensures the immediate caller is the owner
// Transfer funds...
}
}Missing Access Control
For example, a contract might include a function to pause all operations, but without restricting who can call it. One of the most common security issues in NEAR contracts is exposing admin-level or internal functions without proper access control.
// VULNERABLE: No access control on critical function
pub fn pause(&mut self) {
self.status = ContractStatus::Paused;
}
// SECURE: With access control
pub fn pause(&mut self) {
assert!(
env::predecessor_account_id() == self.owner,
"Not allowed"
);
self.status = ContractStatus::Paused;
}Without these checks, anyone could pause the contract, leading to denial-of-service conditions or other unintended behaviour.
Protecting Callback Functions
Cross-contract calls in NEAR are asynchronous, creating a plausible vulnerability between the initial call and its callback execution. As laid out in more detail in our earlier article, callbacks must be protected to prevent unauthorized access.
The #[private] macro is essential for callback protection:
// VULNERABLE: Unprotected callback
pub fn ft_resolve_transfer(
&mut self,
sender_id: AccountId,
receiver_id: AccountId,
amount: U128,
) -> U128 {
// A malicious actor could call this directly with
// arbitrary parameters to drain funds
// Process transfer result...
}
// SECURE: Protected callback with #[private] macro
#[private]
pub fn ft_resolve_transfer(
&mut self,
sender_id: AccountId,
receiver_id: AccountId,
amount: U128,
) -> U128 {
// Only the contract itself can call this function
// Process transfer result...
}The #[private] macro expands to a check that keeps current_account_id() == predecessor_account_id(), which is intended to guarantee that only the contract itself can call the function.
Material Edge Case: Account Key Bypass
There is a subtle but material edge case to be aware of with the #[private] macro. If the contract account (e.g., alice.near) has access keys (either Full Access or Function Call keys) that can call the particular function, then the account holder could potentially bypass the #[private] protection.
This is possible because:
- When the account uses its keys to call its own contract's function straight both
current_account_id()andpredecessor_account_id()will return the same value (alice.near).
Macro Usage and Trait Implementation Risks
NEAR's Rust SDK uses macros like #[near] to generate boilerplate code. Still, improper use of these macros can create security vulnerabilities.
A notably subtle issue involves trait implementations. When marking a trait build-out with #[near], all methods in that trait are exposed for external calls, even if some were meant to be internal only:
pub trait Pausable {
fn toggle_pause(&mut self);
fn pause(&mut self);
fn unpause(&mut self);
fn when_not_paused(&self);
}
// VULNERABLE: Exposes all trait methods to external calls
#[near]
impl Pausable for StatusMessageContract {
fn toggle_pause(&mut self) {
// Anyone can call this function even though it should be restricted
// ...
}
// ...
}
// SECURE: Use internal traits without #[near] or use explicit access control
#[near]
impl StatusMessageContract {
pub fn pub_toggle_pause(&mut self) {
assert!(env::predecessor_account_id() == self.owner, "Permission Denied");
self.toggle_pause()
}
fn toggle_pause(&mut self) {
// Internal implementation that can't be called directly
// ...
}
}Best Practices for Implementation Organization
- Build sensitive traits in private functions, then wrap them with public functions that include access control
// Good pattern: Separate trait implementation from contract interface
// Trait implementation without #[near] - not directly callable
impl Pausable for StatusMessageContract {
fn toggle_pause(&mut self) {
// Internal implementation
self.paused = !self.paused;
}
// Other trait methods...
}
// Public interface with #[near] - contains access control
#[near]
impl StatusMessageContract {
pub fn admin_toggle_pause(&mut self) {
assert!(env::predecessor_account_id() == self.owner, "Permission Denied");
// Call the trait implementation
self.toggle_pause();
}
// Other public contract methods...
}You can use to check how the macros expand and keep they do not expose unintended functionality.
Note #[near] replaces the deprecated macro #[near_bindgen] with some minor differences. Checkout the for the exact changes.
The One-Yocto Pattern
One elegant security pattern in NEAR's ecosystem is the "assert_one_yocto()" path. Since Function Call Keys cannot attach NEAR tokens to transactions, requiring even a tiny token attachment (1 yocto = 10^-24 NEAR) in a useful way keeps that the action must be authorized by a Full Access Key or another contract.
pub fn withdraw_funds(&mut self) {
// Require 1 yocto attachment to ensure this is called with a full access key
assert_one_yocto();
// Perform sensitive operation...
}This creates a clear distinction between actions that should call for full authority and those that can be delegated to Function Call Keys.
Role-Based Access Control (RBAC)
If that key is compromised, the full system is vulnerable. Relying on a single key for all privileged operations creates centralization risk. Role-based access control divides privileges among different accounts based on their role.
// RISKY: Single owner model
pub fn blacklist_account(&mut self, account: AccountId) -> String {
self.assert_owner();
format!("Account Blacklisted: {account}")
}
// SAFER: Role-based access control
pub fn blacklist_account(&mut self, account: AccountId) -> String {
self.assert_security_role();
format!("Account Blacklisted: {account}")
}
pub fn set_price(&mut self, price: u128) -> String {
self.assert_oracle_role();
format!("Price set: {price}")
}
pub fn remove_pool(&mut self, pool_id: u128) -> String {
self.assert_manager_role();
format!("Pool removed: {pool_id}")
}For severe applications, look at implementing multi-signature requirements for sensitive operations or transitioning to DAO governance for the most material functions.
NEAR-specific RBAC implementations can be found in libraries like or .
Centralization Risks with Full Access Keys
Smart contracts controlled by Full Access Keys stand for a centralisation risk. If that key is compromised, the whole contract can be replaced or drained, since NEAR allows updating contract code after deployment (unlike Ethereum). For truly decentralized applications, look at these approaches:
- Remove all keys after deployment (entering a "No Access Key" state)
- Put in place time-locks on sensitive operations
- Call for multi-signature approval for code marks
- Transition to DAO governance
A contract with no access keys can still be interacted with, but no single entity can modify its code or delete it.
The simplest and most reliable method is to use a DeleteKey action in a transaction after the contract is deployed:
// Delete the specified key
await account.deleteKey(publicKeyToRemove);Conclusion
By implementing human-readable accounts, hierarchical structures, and granular permission systems, NEAR has created an infrastructure that is both powerful for developers and accessible to users. NEAR's account system stands for a thoughtful balance between security, usability, and flexibility.
At NFT Bounty, we are committed to securing and hardening NFT Bounty networks and protocols of all kinds. If you are building solutions and want to harness our cutting-edge security expertise in this area, get in touch!