Getting Started
Contract Basics
Solidity is the primary language for Ethereum smart contracts. pragma sets the compiler version. A contract is like a class. Functions can be public, private, view (read-only), or payable.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 storedData;
function set(uint256 x) public {
storedData = x;
}
function get() public view returns (uint256) {
return storedData;
}
}Pragma & Compiler Version
pragma specifies the compiler version. ^0.8.0 allows any 0.8.x patch. For production, lock the version exactly (0.8.19) to avoid surprises. Always include the pragma at the top of every .sol file.
// Lock to a single version
pragma solidity 0.8.19;
// Allow patches (^0.8.0 means >=0.8.0 <0.9.0)
pragma solidity ^0.8.0;
// Range of versions
pragma solidity >=0.8.0 <0.9.0;
// Experimental features (ABIEncoderV2 in older versions)
pragma experimental ABIEncoderV2;Comments & NatSpec
NatSpec comments (/// or /** */) generate documentation. @notice is for end users, @dev for developers, @param describes parameters, @return describes return values. They are picked up by tools like Etherscan and Remix.
// Single-line comment
/* Multi-line
comment */
/// @title A Simple Contract
/// @author Alice
/// @notice This is visible to end users
/// @dev This is for developers
contract Commented {
/// @notice Sets the value
/// @param _x The new value
/// @dev Internal details here
function set(uint256 _x) public {}
}SPDX License
Since Solidity 0.6.8, the SPDX license identifier is mandatory at the top of the file. It helps tools and users understand how the code can be reused. Use MIT for permissive open-source, GPL-3.0 for copyleft, or UNLICENSED for private code.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Common licenses:
// MIT - permissive
// GPL-3.0 - copyleft
// Apache-2.0 - permissive with patent grant
// UNLICENSED - private/no license
// BSD-2-Clause, BSD-3-Clause
contract Licensed {}File Structure & Imports
Imports bring in contracts, libraries, or interfaces. Use {Name} to import only specific items. Relative paths work as expected. The 'is' keyword establishes inheritance. Files typically have one main contract but may contain several.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./OtherContract.sol";
import {SpecificContract, Helper} from "./Library.sol";
import * as Lib from "./Library.sol"; // namespace
import "openzeppelin/contracts/token/ERC20.sol";
contract MyContract is OtherContract {
// ...
}Value Types
Integers (uint / int)
uint = unsigned int (no negatives), int = signed. Sizes range from 8 to 256 in steps of 8. uint defaults to uint256. Since 0.8.0, arithmetic auto-checks for overflow/underflow and reverts. Use type(T).max / type(T).min to get bounds.
uint256 public bigNumber = 1 ether; // 10^18
uint8 public small = 255; // max 255
int256 public signed = -100;
uint public defaultSize = 42; // uint256 by default
// Arithmetic
uint256 a = 10;
uint256 b = 3;
uint256 sum = a + b; // 13
uint256 diff = a - b; // 7 (0.8+ reverts on underflow)
uint256 product = a * b; // 30
uint256 quotient = a / b; // 3
uint256 remainder = a % b; // 1
uint256 power = a ** 2; // 100
// Type min/max (0.8+)
uint256 max = type(uint256).max;
int256 min = type(int256).min;Boolean
bool can only be true or false. && and || short-circuit, meaning the right side is only evaluated if needed. Default value is false. Booleans cost more gas than uint256 in some contexts (storage packing).
bool public flag = true;
bool public negated = !flag; // false
bool public and = flag && false; // false
bool public or = flag || false; // true
// Short-circuit evaluation
bool result = (flag && expensiveCall()); // skips call if flag false
bool result2 = (!flag || expensiveCall());
// Equality
bool equal = (1 == 1); // true
bool notEq = (1 != 2); // true
// Boolean defaults to false
bool public defaultValue; // falseAddress
address holds a 20-byte Ethereum address. address payable can receive Ether (has transfer/send methods). Use payable(x) to convert. Addresses have .balance property (in wei). Always verify address literals against EIP-55 checksum to catch typos.
address public owner = msg.sender;
address payable public treasury = payable(owner);
// Balance (read-only)
uint256 bal = owner.balance; // in wei
// Send Ether (payable address only)
treasury.transfer(1 ether);
bool sent = treasury.send(1 ether);
// Convert from address to payable
address addr = 0x123...;
address payable payableAddr = payable(addr);
// Address literals (checksummed)
address public constant BURN = 0x000000000000000000000000000000000000dEaD;
// Compare addresses
bool same = (addr1 == addr2);Bytes & Byte Arrays
Fixed bytes (bytes1..bytes32) are cheap and used for hashes/selectors. Dynamic bytes is for raw binary data; string is for UTF-8 text. Use bytes32 for hashes (keccak256 returns bytes32). bytes.concat() (0.8.4+) is cheaper than abi.encodePacked for byte arrays.
// Fixed-size byte arrays (bytes1 to bytes32)
bytes32 public hash = keccak256(abi.encodePacked("data"));
bytes1 public b1 = 0x41; // single byte
bytes4 public selector = bytes4(keccak256("transfer(address,uint256)"));
// Dynamic byte arrays
bytes public dynamic = "hello";
bytes public empty;
// String vs bytes
string public text = "Hello"; // UTF-8 string
bytes public raw = bytes(text);
// Operations on fixed bytes
bytes32 data = keccak256(abi.encode("x"));
bytes1 first = data[0]; // access by index
uint256 length = data.length; // 32
// Concatenation
bytes memory combined = bytes.concat(bytes("a"), bytes("b"));Enums
Enums define a finite set of named values. They are stored as uint8 (max 256 members). Default value is the first member (index 0). Convert between enum and uint using casts. type(EnumName).max gives the highest member.
contract EnumExample {
enum Status { Pending, Active, Paused, Closed }
Status public status = Status.Pending;
function activate() public {
require(status == Status.Pending, "Not pending");
status = Status.Active;
}
function next() public {
// Cast to uint to do math
uint current = uint(status);
if (current < uint(type(Status).max)) {
status = Status(current + 1);
}
}
// Default value is the first member (Pending)
Status public defaultValue; // Pending
}Type Conversion & Casting
Implicit conversions only happen when no data loss is possible (uint8 -> uint256). For narrowing conversions (uint256 -> uint8) or signed/unsigned, use explicit casts. Converting a negative int to uint produces a huge number due to two's complement.
// Implicit conversion (safe, widening)
uint8 small = 10;
uint256 big = small; // OK: uint8 fits in uint256
// Explicit conversion (narrowing, may lose data)
uint256 large = 300;
uint8 narrow = uint8(large); // truncates to 44 (300 % 256)
// Address conversion
address addr = 0xAb8483F64d9C6d1Ec37F8D2C4fA6bA5d5C5e5C5e;
address payable payableAddr = payable(addr);
// Bytes conversion
bytes32 b32 = bytes32(uint256(42));
uint256 num = uint256(b32);
bytes4 selector = bytes4(keccak256("foo()"));
// String <-> bytes
string memory s = "hi";
bytes memory bs = bytes(s);
string memory s2 = string(bs);
// int <-> uint (explicit, watch for negatives)
int256 signed = -5;
uint256 unsigned = uint256(signed); // huge number!Reference Types
String
Strings in Solidity are UTF-8 byte arrays — length() returns byte count, not character count (emoji/multibyte chars differ). Equality must be checked via keccak256 hash. string.concat (0.8.12+) is cheaper than abi.encodePacked. Strings are expensive in gas.
contract Strings {
string public greeting = "Hello, World!";
function concat(string memory a, string memory b)
public pure returns (string memory)
{
return string.concat(a, " ", b); // 0.8.12+
}
function length(string memory s) public pure returns (uint256) {
return bytes(s).length; // byte length, not char count
}
function equals(string memory a, string memory b)
public pure returns (bool)
{
return keccak256(bytes(a)) == keccak256(bytes(b));
}
// Storage string (state variable)
string public stored;
function set(string memory s) public { stored = s; }
}Arrays (Fixed & Dynamic)
Fixed arrays [N] have a compile-time size; dynamic arrays [] grow with push/pop. Memory arrays must be created with 'new Type[](size)'. Reading/writing storage arrays costs gas; prefer memory for temporary data. push() returns a reference in 0.6+.
contract Arrays {
// Fixed-size array
uint256[5] public fixedArr = [1, 2, 3, 4, 5];
// Dynamic array
uint256[] public dynamicArr;
function pushPop() public {
dynamicArr.push(10); // append: [10]
dynamicArr.push(20); // [10, 20]
dynamicArr.pop(); // remove last: [10]
}
function getLength() public view returns (uint256) {
return dynamicArr.length;
}
function iterate() public view returns (uint256 sum) {
for (uint256 i = 0; i < dynamicArr.length; i++) {
sum += dynamicArr[i];
}
}
// Array of arrays (2D)
uint256[][] public matrix;
// Memory array (must have fixed length when created)
function memArray() public pure returns (uint256[] memory) {
uint256[] memory arr = new uint256[](3);
arr[0] = 1; arr[1] = 2; arr[2] = 3;
return arr;
}
}Structs
Structs group related fields. They can be stored in storage, memory, or passed as function arguments. Define them at file or contract level. Initializing with named fields {field: value} is clearer; positional is shorter. Mapping values cannot be struct directly accessed as a whole in older versions.
contract Structs {
struct User {
address wallet;
uint256 balance;
bool active;
string name;
}
// Single struct (storage)
User public owner;
User[] public users; // array of structs
mapping(address => User) public userByAddr;
function createUser(string memory name) public {
User memory u = User({
wallet: msg.sender,
balance: 100,
active: true,
name: name
});
users.push(u);
}
// Shorter syntax
function createShort() public {
users.push(User(msg.sender, 50, true, "anon"));
}
// Update a field
function deactivate(uint256 index) public {
users[index].active = false; // direct storage write
}
}Mappings
Mappings are hash tables: keccak256(key) => value. They have no length and cannot be iterated. Keys can be value types (uint, address, bytes, enum); values can be anything. Nested mappings allow multi-key lookups. To iterate, maintain a separate array of keys. delete resets a key to its zero value.
contract Mappings {
// keyType => valueType
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowance;
mapping(address => bool) public isRegistered;
function setBalance(uint256 amount) public {
balances[msg.sender] = amount;
}
function approve(address spender, uint256 amount) public {
allowance[msg.sender][spender] = amount;
}
function register() public {
isRegistered[msg.sender] = true;
}
// Iterable mapping pattern
address[] public userList;
mapping(address => bool) public exists;
function addUser(address u) public {
if (!exists[u]) {
userList.push(u);
exists[u] = true;
}
}
// CANNOT iterate or get length of a mapping directly
// CANNOT delete a mapping, only individual keys
function remove() public {
delete balances[msg.sender]; // sets to 0
}
}Data Locations (storage / memory / calldata)
storage = persistent on-chain state (expensive). memory = temporary, lives during function call (cheap). calldata = read-only, only for external function inputs (cheapest). Always use calldata for external function args you only read. Default location: state vars are storage; function local complex types default to memory.