NEAR contract security checklist
A practical NEAR smart contract security review checklist: callbacks, access, deposits, storage, gas and upgrades — plus how to test failure paths in sandbox.
Intermediate5 min read3-question check
Use this page as a review pass before every deploy. Each row is a bug that has cost real projects money; each links to the lesson with the vulnerable and fixed code. A checklist does not replace threat modeling, tests or an audit — it catches the well-known mistakes so reviewers can spend their time on the novel ones.
Review checklist#
| Area | Check | Lesson |
|---|---|---|
| Async | Every value-moving cross-contract call has a callback that handles the Err branch and undoes optimistic state | Callbacks & rollbacks |
| Async | Balances are deducted before the call and restored on failure; deposits are credited only after success | Reentrancy across receipts |
| Async | State is consistent at the end of every receipt; locks or pending flags are released on both callback branches | Reentrancy across receipts |
| Async | Every callback is #[private], has no unwrap in the undo path, and has enough static gas for it | Private callbacks |
| Access | Authorization uses env::predecessor_account_id(), never signer_account_id() | Predecessor vs signer |
| Access | Contract account keys reviewed; no stray full-access keys; no unguarded add-key or deploy methods | Access keys |
| Access | Asset-moving and permission-changing methods are #[payable] and call assert_one_yocto() | One yocto |
| Money | #[payable] only where needed; exact price checks; excess deposits refunded; outgoing transfers that matter have a callback | Deposits & refunds |
| Money | Callers pay for the storage they add (per write or NEP-145); user-provided strings and vectors are capped | Storage attacks |
| State | overflow-checks = true in [profile.release]; balances use checked_* math with clear require! messages | Overflow & panics |
| State | No change method iterates a collection that users can grow; list views are paginated with a hard cap | Unbounded iteration |
| Upgrades | State layout changes ship with a #[private] #[init(ignore_state)] migrate; upgrade authority is a DAO/multisig or the contract is locked | Upgrades & migrations |
Test the failure paths#
Unit tests rarely catch these bugs, because they do not run real receipts. Integration tests with near-workspaces deploy your Wasm to a local sandbox node, so callbacks, deposits, gas limits and access checks behave as on mainnet. Write a test for every failure you care about, not just the happy path.
use serde_json::json;
#[tokio::test]
async fn callback_rejects_strangers() -> anyhow::Result<()> {
let wasm = near_workspaces::compile_project("./").await?;
let sandbox = near_workspaces::sandbox().await?;
let contract = sandbox.dev_deploy(&wasm).await?;
let attacker = sandbox.dev_create_account().await?;
contract
.call("new")
.args_json(json!({ "token": "token.test.near" }))
.transact()
.await?
.into_result()?;
// #[private] must make this fail
let res = attacker
.call(contract.id(), "on_withdraw")
.args_json(json!({ "user": attacker.id(), "amount": "1000000" }))
.transact()
.await?;
assert!(res.is_failure());
Ok(())
}Before mainnet#
- Read the official NEAR security docs — including front-running, sybil resistance and duplicate inputs, which this module does not cover.
- Get an independent audit from a firm with NEAR and Rust experience before holding meaningful user funds, and re-audit after significant changes.
- Publish verifiable source (NEP-330 metadata + reproducible builds) so users can check what is deployed.
- Decide upgrade authority and key ownership up front, document it, and review the contract account’s keys after every deploy.
- Keep a NEAR buffer on the contract account for storage, and monitor it.
Check yourself
3 questions · progress saved in this browser