What is Solidity?
Solidity is a statically-typed, contract-oriented programming language designed for writing smart contracts on the Ethereum Virtual Machine (EVM). Its syntax is influenced by C++, Python, and JavaScript, making it approachable for developers coming from those backgrounds. Solidity compiles to EVM bytecode, which is then deployed to and executed on the blockchain.
Solidity Key Features
- Static Typing: Variables must be declared with types, catching errors at compile time
- Inheritance: Contracts can inherit from other contracts, enabling code reuse
- Libraries: Reusable code that can be deployed once and called by many contracts
- Custom Modifiers: Reusable function guards for access control and validation
- Events: First-class logging mechanism for off-chain communication
Data Types
Solidity provides a rich type system. Understanding these types and their gas costs is essential for writing efficient contracts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract DataTypes {
// Value Types
bool public isActive = true;
uint256 public totalSupply = 1000000; // Unsigned integer (0 to 2^256-1)
int256 public temperature = -10; // Signed integer
address public owner = msg.sender; // 20-byte Ethereum address
bytes32 public dataHash; // Fixed-size byte array
// Address with payable — can receive ETH
address payable public treasury;
// Enums — user-defined type with named constants
enum Status { Pending, Active, Closed }
Status public currentStatus = Status.Pending;
// Reference Types
string public name = "My Token"; // Dynamic string (UTF-8)
bytes public rawData; // Dynamic byte array
// Arrays
uint256[] public dynamicArray; // Dynamic-size array
uint256[10] public fixedArray; // Fixed-size array
// Mappings — hash table for key-value pairs
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances; // Nested
// Structs — custom data structures
struct User {
address wallet;
string username;
uint256 balance;
bool isVerified;
}
mapping(address => User) public users;
// Constants and Immutables — gas optimizations
uint256 public constant MAX_SUPPLY = 10_000_000; // Set at compile time
uint256 public immutable deployTime; // Set once in constructor
constructor() {
deployTime = block.timestamp;
treasury = payable(msg.sender);
}
}
Functions and Visibility
Functions in Solidity have visibility specifiers that control who can call them, and state mutability keywords that indicate how they interact with the blockchain state.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Functions {
uint256 private value;
address public owner;
constructor() {
owner = msg.sender;
}
// external — only callable from outside the contract
function setValue(uint256 _value) external {
value = _value;
}
// public — callable both externally and internally
function getValue() public view returns (uint256) {
return value;
}
// internal — only this contract and derived contracts
function _doubleValue() internal view returns (uint256) {
return value * 2;
}
// private — only this contract (not even derived)
function _validate(uint256 _val) private pure returns (bool) {
return _val > 0 && _val < 1000000;
}
// view — reads state but does not modify it (no gas when called externally)
function getDoubled() external view returns (uint256) {
return _doubleValue();
}
// pure — does not read or modify state
function add(uint256 a, uint256 b) external pure returns (uint256) {
return a + b;
}
// payable — can receive ETH with the call
function deposit() external payable {
require(msg.value > 0, "Must send ETH");
}
// Multiple return values
function getInfo() external view returns (uint256, address, bool) {
return (value, owner, value > 0);
}
}
Control Flow and Error Handling
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ControlFlow {
// Custom errors (gas-efficient since Solidity 0.8.4)
error InsufficientBalance(uint256 available, uint256 required);
error Unauthorized(address caller);
mapping(address => uint256) public balances;
address public owner;
constructor() {
owner = msg.sender;
}
function transfer(address to, uint256 amount) external {
// require — validates input/state, reverts with message
require(to != address(0), "Cannot transfer to zero address");
// Custom error — more gas efficient than string messages
if (balances[msg.sender] < amount) {
revert InsufficientBalance(balances[msg.sender], amount);
}
// If-else
if (amount > 1000) {
// Large transfer logic
} else if (amount > 0) {
// Normal transfer logic
} else {
revert("Amount must be positive");
}
balances[msg.sender] -= amount;
balances[to] += amount;
}
// For loops (be careful with gas!)
function sumArray(uint256[] calldata arr) external pure returns (uint256) {
uint256 total = 0;
for (uint256 i = 0; i < arr.length; i++) {
total += arr[i];
}
return total;
}
// While loop
function findIndex(uint256[] calldata arr, uint256 target)
external pure returns (int256)
{
uint256 i = 0;
while (i < arr.length) {
if (arr[i] == target) return int256(i);
i++;
}
return -1;
}
// assert — check for internal errors / invariants
function withdraw(uint256 amount) external {
uint256 previousBalance = balances[msg.sender];
balances[msg.sender] -= amount;
// This should always hold — if it doesn't, something is very wrong
assert(balances[msg.sender] <= previousBalance);
}
// try/catch for external calls
function safeCall(address target) external returns (bool) {
try IExternal(target).doSomething() returns (uint256 result) {
// Handle success
return true;
} catch Error(string memory reason) {
// Handle require/revert with message
return false;
} catch (bytes memory) {
// Handle low-level errors
return false;
}
}
}
interface IExternal {
function doSomething() external returns (uint256);
}
Modifiers and Inheritance
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Base contract with access control
abstract contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previous, address indexed next);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // Placeholder for the function body
}
constructor() {
owner = msg.sender;
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "Invalid address");
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
// Pausable adds pause/unpause functionality
abstract contract Pausable is Ownable {
bool public paused;
event Paused(address account);
event Unpaused(address account);
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
function pause() external onlyOwner {
paused = true;
emit Paused(msg.sender);
}
function unpause() external onlyOwner {
paused = false;
emit Unpaused(msg.sender);
}
}
// Final contract inherits both Ownable and Pausable
contract MyToken is Pausable {
mapping(address => uint256) public balances;
uint256 public totalSupply;
event Transfer(address indexed from, address indexed to, uint256 amount);
// Combining modifiers — both must pass
function transfer(address to, uint256 amount)
external
whenNotPaused
{
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
// onlyOwner inherited from Ownable through Pausable
function mint(address to, uint256 amount) external onlyOwner {
totalSupply += amount;
balances[to] += amount;
emit Transfer(address(0), to, amount);
}
}
Interfaces and Abstract Contracts
Interfaces define the external API of a contract without implementation. They are essential for interacting with other deployed contracts and for defining standards like ERC-20 and ERC-721. Abstract contracts can contain some implementation but leave certain functions unimplemented for child contracts to define.
Best Practices for Solidity
- Use latest compiler: Always use Solidity 0.8.x or later for built-in overflow checks
- Custom errors over strings: They save gas and provide structured error data
- Use calldata for read-only params: Cheaper than memory for external function parameters
- Mark functions correctly: Use view/pure when possible; use external over public when the function is not called internally
- Avoid unbounded loops: Gas limits can cause transactions to fail if loops iterate too many times
- Use events for tracking: Emit events for all important state changes to enable off-chain indexing