Storage staking attacks
NEAR storage cost attacks: if anyone can make your contract store data, they lock your balance. Charge per byte, use NEP-145 deposits and cap input sizes.
Advanced7 min read3-question check
On NEAR, an account must hold a balance proportional to the bytes it stores — storage staking. That NEAR is locked, not spent: it cannot pay for gas or be transferred while the data exists. The cost is paid by the contract account, not by whoever caused the write.
So any method that lets a stranger add data to your state is a method that lets a stranger lock up your NEAR. Push enough bytes and the contract cannot cover its storage at all, and every write that grows state starts failing.
The bug#
Rust
#[near]
impl Guestbook {
// BUG: anyone can call this as often as they like, with a
// message of any length. Each byte locks the contract's NEAR.
pub fn add_message(&mut self, text: String) {
self.messages.push(Message {
author: env::predecessor_account_id(),
text,
});
}
}The fix#
Rust
use near_sdk::store::Vector;
use near_sdk::{env, near, require, AccountId, NearToken, PanicOnDefault, Promise};
const MAX_TEXT_LEN: usize = 280;
#[near(serializers = [borsh])]
pub struct Message {
author: AccountId,
text: String,
}
#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Guestbook {
messages: Vector<Message>,
}
#[near]
impl Guestbook {
#[payable]
pub fn add_message(&mut self, text: String) {
require!(text.len() <= MAX_TEXT_LEN, "message too long");
let author = env::predecessor_account_id();
let before = env::storage_usage();
self.messages.push(Message { author: author.clone(), text });
// store:: collections write lazily; flush so the bytes are counted now
self.messages.flush();
let bytes = env::storage_usage().saturating_sub(before);
let cost = env::storage_byte_cost().saturating_mul(bytes as u128);
let deposit = env::attached_deposit();
require!(deposit >= cost, "attach enough NEAR to cover storage");
let refund = deposit.saturating_sub(cost);
if refund > NearToken::from_yoctonear(0) {
Promise::new(author).transfer(refund);
}
// if this panics, the push above is reverted along with everything else
}
}- List every method that can grow state. Who pays for each byte?
- Bound every user-supplied
StringandVecwithrequire!. - Refund excess deposit, and refund stake when data is removed.
- Keep a NEAR buffer on the contract account for its own storage, and monitor it.
Check yourself
3 questions · progress saved in this browser