Skip to content

General Security Considerations

While Solidity itself provides various security guarantees, writing smart contracts requires a deep understanding of the underlying execution environment. Contracts are completely public, meaning their source code and compiled bytecode can be inspected by anyone, and anyone can interact with them.

💡 NeoVM Difference

The most critical security differences between EVM and NeoVM lie in Authorization (Witnesses vs msg.sender) and Contract Permissions (Manifests).

Pitfalls

Authorization: Witnesses over msg.sender

On Ethereum, authorization is implicit through msg.sender. The caller's address is automatically set by the EVM to the contract or account that initiated the specific CALL.

On Neo, authorization is explicit. The transaction includes cryptographic witnesses (signatures) that prove the caller controls specific addresses.

While Neo DevPack for Solidity maps msg.sender to Runtime.getCallingScriptHash() (which securely returns the hash of the contract calling your contract), it is an anti-pattern to use this for user authorization. You must use Runtime.checkWitness().

solidity
// ❌ EVM pattern — relies on msg.sender implicitly
function withdraw(uint256 amount) public {
    require(msg.sender == owner, "not owner");
    // ...
}

// ✅ Neo-idiomatic — explicit witness check
import {Runtime} from "@neo/Runtime.sol";

function withdraw(uint256 amount) public {
    // Cryptographically verifies the transaction was signed by `owner`
    require(Runtime.checkWitness(owner), "not authorized");
    // ...
}

If you rely on msg.sender == user, your contract is vulnerable to users who wrap their calls in another contract (because msg.sender will become the intermediate contract, not the user). Runtime.checkWitness() verifies the user authorized the transaction globally, regardless of the call depth.

Manifest Permissions and Wildcards

Neo N3 enforces contract call permissions at the VM level via the contract's manifest. If your contract attempts to call a target contract or method not explicitly allowed in your manifest, execution will fault.

By default, dynamic calls in Solidity (like address(target).call(...)) force the compiler to emit wildcard permissions ("contract": "*", "methods": "*"). This is a severe security risk, as it allows your contract to be hijacked to interact with malicious contracts, effectively bypassing the platform's permission sandbox.

Checked Arithmetic and Wraparound

NeoVM uses arbitrary-precision BigInteger representation internally, but Neo DevPack for Solidity emits Solidity 0.8 fixed-width overflow and underflow guards outside unchecked blocks. Checked overflows revert with Panic(0x11).

If your contract's security model relies on wraparound, put the operation in an unchecked block and cover that behavior with tests. unchecked does not suppress division-by-zero or modulo-by-zero panics.

Upgradeability

Ethereum relies on delegatecall and complex proxy architectures to make contracts upgradeable. NeoVM inherently blocks delegatecall because it isolates storage context strictly per-contract.

However, Neo natively supports upgrading contracts using ContractManagement.update(). This replaces the executing code while retaining the existing storage.

Recommendations

1. Compile with Strict Permissions

Always compile production contracts with strict wildcard denial flags:

bash
neo-solc MyContract.sol --deny-wildcard-contracts --deny-wildcard-methods

If dynamic routing is strictly required by your architecture, use NatSpec overrides (@custom:neo.manifest.permissions) to explicitly allowlist specific trusted hashes rather than defaulting to *.

2. Guard the Upgrade Method

Because ContractManagement.update() is a native intrinsic that modifies the code of the contract, ensure that access to your update method is heavily protected. It should ideally be behind a multisig or DAO governance check, relying on Runtime.checkWitness().

3. Handle NEP-17 Payments Carefully

When implementing onNEP17Payment (the equivalent to Ethereum's receive()), always verify the msg.sender to ensure the token being received is the one you expect (e.g., ensuring msg.sender == NativeContracts.GAS). Otherwise, malicious users can send worthless tokens to spoof deposits.

MIT Licensed