Introduction to Smart Contracts
A Simple Smart Contract
Let us begin with the most basic example. It is fine if you do not understand everything right now, we will go into more depth later.
Storage Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.34;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}The first line tells you that the source code is licensed under the MIT license. Machine-readable license specifiers are important in a setting where publishing source code is the default.
The next line specifies that the source code is written for Solidity version 0.8.34 or newer.
A contract in the sense of Solidity is a collection of code (its functions) and data (its state) that resides at a specific address on the blockchain. The line uint storedData; declares a state variable of type uint (unsigned integer). You can think of it as a single slot in a database that you can query and alter by calling functions of the code that manages the database.
💡 NeoVM Difference: Storage
On Ethereum, state variables are stored in sequential 256-bit slots. On NeoVM, they are stored in a dynamic Key-Value database where the key is deterministically generated from the variable's name (SHA256("storedData")).
Subcurrency Example
The following contract implements the simplest form of a cryptocurrency.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.34;
import {Runtime} from "@neo/Runtime.sol";
contract Coin {
// The keyword "public" makes variables
// accessible from other contracts
address public minter;
mapping(address => uint) public balances;
// Events allow clients to react to specific
// contract changes you declare
event Sent(address from, address to, uint amount);
// Constructor code is only run when the contract
// is created
constructor() {
minter = Runtime.getCallingScriptHash();
}
// Sends an amount of newly created coins to an address
// Can only be called by the contract creator
function mint(address receiver, uint amount) public {
require(Runtime.checkWitness(minter), "Not the minter");
balances[receiver] += amount;
}
// Sends an amount of existing coins
// from any caller to an address
function send(address receiver, uint amount) public {
address sender = Runtime.getCallingScriptHash();
require(Runtime.checkWitness(sender), "Not authorized");
require(amount <= balances[sender], "Insufficient balance");
balances[sender] -= amount;
balances[receiver] += amount;
emit Sent(sender, receiver, amount);
}
}This contract introduces address and mapping. The address type is a 20-byte value (a Neo script hash). The mapping type is essentially a hash table.
💡 NeoVM Difference: Authorization
Notice the use of Runtime.checkWitness(minter) instead of Ethereum's require(msg.sender == minter). Neo relies on cryptographic transaction witnesses to verify that a specific user has authorized an action, rather than tracking the immediate caller address.
Blockchain Basics
Blockchains as a concept are not too hard to understand. The reason why they are a bit difficult to wrap your head around is that they combine three concepts: cryptography, consensus, and peer-to-peer networking.
Transactions
A transaction is a message that is sent from one account to another account. It can include a binary data payload (which executes a smart contract) and a cryptographic signature (witness).
Blocks
One major problem to overcome is double spending. To solve this, transactions are grouped into blocks. On Neo N3, blocks are generated by consensus nodes using the Delegated Byzantine Fault Tolerance (dBFT) algorithm.
Unlike Ethereum's Proof-of-Stake or Proof-of-Work, dBFT provides single-block finality. Once a block is committed to the Neo blockchain, it cannot be reverted. There are no forks or reorganizations.
The Neo Virtual Machine (NeoVM)
Overview
The Neo Virtual Machine (NeoVM) is the runtime environment for smart contracts in Neo. It is not a port of the Ethereum Virtual Machine (EVM); it is a completely distinct architecture. While the neo-devpack-solidity compiler allows you to write standard Solidity, understanding the NeoVM is crucial for writing efficient and secure contracts.
Accounts
On Neo, there are two types of accounts, though they share the same 20-byte address format (Script Hash):
- Standard Accounts: Controlled by public-private key pairs (signatures).
- Contract Accounts: Controlled by the code of a deployed smart contract.
Dual Token Model (NEO and GAS)
Neo utilizes a dual-token model:
- NEO: The governance token. It is indivisible (no decimals) and is used to vote for consensus nodes.
- GAS: The utility token. It has 8 decimals and is used to pay for network transactions and smart contract execution.
💡 NeoVM Difference: No Native Ether
Ethereum attaches an intrinsic Ether balance to every address, manipulated via msg.value and address.transfer(). Neo does not have a native balance property. NEO and GAS are implemented as standard NEP-17 smart contracts. Value transfers require explicitly calling the GAS or NEO smart contracts.
Gas and Fees
On Neo N3, the cost of a transaction is split into two parts:
- System Fee (Execution Gas): Pays for the actual execution of NeoVM opcodes and syscalls.
- Network Fee: Pays for the byte size of the transaction and signature verification.
Storage, Memory, and the Stack
NeoVM has a different memory model than EVM:
- Storage: A persistent Key-Value database mapping byte arrays to byte arrays. It is relatively expensive to read and write.
- Stack: NeoVM evaluates logic using an execution stack. Items pushed to the stack are fully typed (e.g., an
Integer, anArray, aMap, or aByteArray). - Memory: NeoVM does not have a linear memory space. When Solidity allocates an array or struct in
memory, it is actually creating a typedArrayitem on the NeoVM execution stack.
Message Calls and Syscalls
Contracts communicate with each other through Message Calls. Instead of arbitrary binary payloads with 4-byte selectors, NeoVM invokes methods using their exact string name via System.Contract.Call.
Furthermore, NeoVM exposes Syscalls (System Calls) which allow contracts to interact with the underlying blockchain (e.g., getting the current block, performing cryptography, or reading storage).
Delegatecall / Callcode and Libraries
There exists a special variant of a message call on Ethereum, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context (i.e. at the address) of the calling contract.
🚫 Unsupported Feature: Delegatecall and Callcode
NeoVM does not support delegatecall or callcode. Every contract has entirely isolated storage. You cannot execute another contract's code in your contract's storage context. Therefore, Ethereum-style proxy upgrade patterns and separately deployed or linkable external libraries are not available. User-defined Solidity libraries compile by inlining calls into the consuming contract.
Events and Notifications
Solidity Events are compiled to NeoVM Notifications (Runtime.Notify). Like EVM logs, these are stored in the transaction receipt and cannot be accessed from within the smart contract itself.
However, unlike EVM, Neo notifications do not utilize "Topics" for indexed parameters. All parameters are grouped into a single state array.
Create
Neo N3 contracts are deployed through the native ContractManagement.deploy() syscall with the compiled .nef bytecode and .manifest.json.
For Solidity source compatibility, new Contract(...) is accepted when the target contract is available in the compilation graph, but it does not deploy a child contract. The compiler inlines/simulates constructor-like logic and returns a zero-address placeholder. Use ContractManagement.deploy(nef, manifest, data) for real deployment.
