Difficulty: Beginner
Solidity is a statically-typed, contract-oriented programming language designed specifically for writing smart contracts on the Ethereum Virtual Machine (EVM). Created by Gavin Wood in 2014 and developed by the Ethereum Foundation's Solidity team, it draws syntax from JavaScript, C++, and Python while introducing blockchain-specific constructs. Solidity is the dominant language for EVM-compatible blockchains, used across Ethereum, Polygon, Arbitrum, Optimism, BNB Chain, Avalanche, and many others.
Every Solidity file begins with a pragma directive that specifies the compiler version. The pragma statement is a safety mechanism that prevents contracts from being compiled with incompatible compiler versions, which could introduce bugs or security vulnerabilities. The most common pattern is `pragma solidity ^0.8.0;`, which means the contract is compatible with any compiler from version 0.8.0 up to (but not including) 0.9.0. Version 0.8.x introduced built-in overflow and underflow checks, making it significantly safer than earlier versions where these errors silently wrapped around.
A contract in Solidity is analogous to a class in object-oriented programming. It encapsulates state variables (persistent data stored on the blockchain), functions (executable code that can read and modify state), events (logging mechanisms for off-chain notification), modifiers (reusable function guards), and custom errors. When a contract is compiled, the Solidity compiler (solc) produces two key outputs: the bytecode (the compiled EVM instructions that get deployed to the blockchain) and the ABI (Application Binary Interface).
The ABI is a JSON specification that describes the contract's public interface - its functions, their parameters, return types, events, and errors. Frontend applications use the ABI to encode function calls into the transaction data field and decode return values and event logs. Without the ABI, interacting with a deployed contract would require manually encoding and decoding binary data according to Ethereum's calling convention. Tools like ethers.js and web3.js accept the ABI and automatically handle this encoding.
The development workflow for Solidity typically involves writing contracts in .sol files, compiling them with solc (usually through frameworks like Hardhat or Foundry), testing them against a local blockchain (Hardhat Network, Anvil, or Ganache), and deploying them to testnets (Sepolia, Goerli) before final deployment to mainnet. Once deployed, a contract's code is immutable - it cannot be changed. This immutability makes thorough testing, formal verification, and security audits essential before deployment.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title HelloWorld - A simple introductory smart contract
/// @notice This contract demonstrates basic Solidity syntax
contract HelloWorld {
// State variable - stored permanently on the blockchain
string public greeting;
// Address of the contract deployer
address public owner;
// Event emitted when the greeting changes
event GreetingChanged(string oldGreeting, string newGreeting, address changedBy);
// Constructor runs once during deployment
constructor(string memory _initialGreeting) {
greeting = _initialGreeting;
owner = msg.sender; // msg.sender is the address deploying the contract
}
// Function to read the greeting (view = read-only, no gas cost when called externally)
function getGreeting() public view returns (string memory) {
return greeting;
}
// Function to update the greeting (state-changing = costs gas)
function setGreeting(string memory _newGreeting) public {
require(bytes(_newGreeting).length > 0, "Greeting cannot be empty");
string memory oldGreeting = greeting;
greeting = _newGreeting;
emit GreetingChanged(oldGreeting, _newGreeting, msg.sender);
}
// Function to get contract's ETH balance
function getBalance() public view returns (uint256) {
return address(this).balance;
}
// Allow the contract to receive ETH
receive() external payable {}
}
This contract demonstrates the fundamental building blocks of Solidity: the pragma directive, state variables (greeting, owner), a constructor that initializes state during deployment, view functions (read-only, no gas when called externally), state-changing functions (modify blockchain state, cost gas), events (for logging), and special functions like receive() for handling ETH transfers. The 'public' keyword on state variables automatically generates getter functions.
import { ethers } from 'ethers';
// After compiling with Hardhat/Foundry, you get the ABI and bytecode
const abi = [
'constructor(string memory _initialGreeting)',
'function greeting() view returns (string)',
'function getGreeting() view returns (string)',
'function setGreeting(string memory _newGreeting)',
'function owner() view returns (address)',
'function getBalance() view returns (uint256)',
'event GreetingChanged(string oldGreeting, string newGreeting, address changedBy)',
];
// The bytecode is the compiled EVM instructions (truncated here)
const bytecode = '0x608060405234801561001057600080fd5b5060405161...';
async function main() {
const provider = new ethers.JsonRpcProvider('http://localhost:8545');
const signer = await provider.getSigner();
// Deploy the contract
const factory = new ethers.ContractFactory(abi, bytecode, signer);
const contract = await factory.deploy('Hello, Blockchain!');
await contract.waitForDeployment();
console.log('Deployed to:', await contract.getAddress());
// Read the greeting (free - no transaction needed)
const greeting = await contract.getGreeting();
console.log('Current greeting:', greeting);
// Update the greeting (costs gas - sends a transaction)
const tx = await contract.setGreeting('Hello from ethers.js!');
const receipt = await tx.wait();
console.log('Greeting updated in block:', receipt.blockNumber);
// Read the updated greeting
const newGreeting = await contract.getGreeting();
console.log('New greeting:', newGreeting);
// Listen for GreetingChanged events
contract.on('GreetingChanged', (oldG, newG, changedBy) => {
console.log(`Greeting changed from "${oldG}" to "${newG}" by ${changedBy}`);
});
}
main();
This shows the full development cycle: deploying a compiled Solidity contract using its ABI and bytecode, reading state (free view calls), modifying state (gas-consuming transactions), and listening for emitted events. The ABI serves as the interface definition that ethers.js uses to encode function calls and decode responses.
Pragma, Contract, Compiler, ABI