Make users pay for their storage
Charge users for the NEAR storage they add: measure env::storage_usage() before and after writes, require a deposit, refund the excess, or use NEP-145.
Intermediate7 min read3-question check
Storage stake is taken from the balance of the account that holds the data: your contract. If users can add records for free, every new record locks a little more of the contract’s NEAR, and a determined user can keep going until the contract cannot afford to store anything and its writes fail with LackBalanceForState.
The fix is to make whoever grows the state pay for it. There are two common shapes: pay per write, where each call attaches a deposit and gets the excess back, and pre-paid storage, where users deposit once into a storage balance that later writes draw from (the NEP-145 standard).
Pay per write, refund the rest#
use near_sdk::store::LookupMap;
use near_sdk::{env, near, require, AccountId, Promise};
const MAX_NOTE_LEN: usize = 280;
#[near(contract_state)]
pub struct Notes {
notes: LookupMap<AccountId, String>,
}
impl Default for Notes {
fn default() -> Self {
Self { notes: LookupMap::new(b"n") }
}
}
#[near]
impl Notes {
#[payable]
pub fn set_note(&mut self, text: String) {
require!(text.len() <= MAX_NOTE_LEN, "note too long");
let initial = env::storage_usage();
self.notes.insert(env::predecessor_account_id(), text);
// store collections buffer writes: flush so storage_usage() sees them
self.notes.flush();
self.charge_storage(initial);
}
}
impl Notes {
fn charge_storage(&self, initial: u64) {
let added = env::storage_usage().saturating_sub(initial);
let cost = env::storage_byte_cost().saturating_mul(added as u128);
let attached = env::attached_deposit();
require!(
attached >= cost,
format!("attach at least {} yoctoNEAR for storage", cost.as_yoctonear())
);
let refund = attached.saturating_sub(cost);
if !refund.is_zero() {
Promise::new(env::predecessor_account_id()).transfer(refund);
}
}
}Capping input size (MAX_NOTE_LEN above) matters as much as the charge: it bounds both the deposit a user can be asked for and the gas a single call can burn. When an update makes a record smaller, the freed stake is unlocked on the contract; if users paid for it, consider returning it too.
Pre-paid storage: NEP-145#
NEP-145 standardises a per-account storage balance so wallets and other contracts know how to register users. A fungible token is the classic example: an account must be registered with storage_deposit before it can receive tokens. The near-contract-standards crate ships a StorageManagement trait for it.
| Method | Kind | Purpose |
|---|---|---|
storage_deposit(account_id, registration_only) | payable change | Register an account and/or top up its storage balance. Anyone can pay for another account. |
storage_withdraw(amount) | change, exactly 1 yoctoNEAR | Withdraw unused (available) storage balance |
storage_unregister(force) | change, exactly 1 yoctoNEAR | Close the registration, delete the data and refund the balance |
storage_balance_of(account_id) | view | Returns { total, available }, or null if not registered |
storage_balance_bounds() | view | Returns { min, max }: what registration costs and the most a contract accepts |
Check yourself
3 questions · progress saved in this browser