The 1 yoctoNEAR guard
Why NEAR contracts require exactly 1 yoctoNEAR with assert_one_yocto(): it forces a full-access-key wallet signature for transfers and other sensitive calls.
Intermediate5 min read3-question check
When a user signs in to a NEAR dApp, the wallet often adds a function-call access key to their account and hands it to the website. That key can call your contract without any wallet prompt. Handy for game moves and likes — dangerous for moving assets.
The standard defence is tiny: require the call to attach exactly 1 yoctoNEAR (10⁻²⁴ NEAR). Function-call keys cannot attach deposits, so the transaction has to be signed with a full-access key — which normally means the user confirms it in their wallet.
The bug#
Rust
#[near]
impl Items {
pub fn transfer_item(&mut self, item_id: u64, receiver_id: AccountId) {
let owner = self.owners.get(&item_id).cloned()
.unwrap_or_else(|| env::panic_str("no such item"));
require!(owner == env::predecessor_account_id(), "not your item");
// BUG: callable with a function-call key. An XSS bug or a malicious
// frontend holding the user's session key can empty their inventory
// without the wallet ever asking.
self.owners.insert(item_id, receiver_id);
}
}The fix#
Rust
use near_sdk::store::LookupMap;
use near_sdk::{assert_one_yocto, env, near, require, AccountId, PanicOnDefault};
#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Items {
owners: LookupMap<u64, AccountId>,
}
#[near]
impl Items {
#[payable] // otherwise near-sdk rejects ANY attached deposit
pub fn transfer_item(&mut self, item_id: u64, receiver_id: AccountId) {
assert_one_yocto(); // panics unless exactly 1 yoctoNEAR is attached
let owner = self.owners.get(&item_id).cloned()
.unwrap_or_else(|| env::panic_str("no such item"));
require!(owner == env::predecessor_account_id(), "not your item");
self.owners.insert(item_id, receiver_id);
}
}Check yourself
3 questions · progress saved in this browser