Solidity Smart Contract Basics: A Beginner's Guide
Solidity Smart Contract Basics: A Beginner's Guide
Solidity is a statically-typed programming language designed for developing smart contracts on the Ethereum blockchain. This guide covers the fundamentals you need to start writing your own smart contracts.
What is a Smart Contract?
A smart contract is a self-executing program stored on the blockchain that runs when predetermined conditions are met. They are:
- Immutable - Cannot be changed once deployed
- Distributed - Runs across all nodes in the network
- Deterministic - Same input always produces same output
Setting Up the Development Environment
Remix IDE
The easiest way to start is with Remix IDE:
- Visit remix.ethereum.org
- Create a new file with
.solextension - Start coding!
Local Development
# Install Node.js, then:
npm install -g hardhat
# Create a new project
npx hardhat init
# Install OpenZeppelin (for secure contract libraries)
npm install @openzeppelin/contracts
Your First Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloWorld {
string public message;
constructor() {
message = "Hello, World!";
}
function setMessage(string memory _message) public {
message = _message;
}
function getMessage() public view returns (string memory) {
return message;
}
}
Solidity Basics
Data Types
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DataTypes {
// Value Types
bool public isTrue = true;
int256 public myInt = -1;
uint256 public myUint = 1;
address public myAddress = 0x742d35Cc6634C0532925a3b844Bc454e4438f44e;
bytes32 public myBytes32 = "Hello";
// Reference Types
string public myString = "Hello World";
bytes public myBytes = "Hello";
uint256[] public myArray = [1, 2, 3];
// Mappings
mapping(address => uint256) public balances;
// Structs
struct User {
string name;
uint256 age;
address wallet;
}
User public user;
// Enums
enum Status { Pending, Active, Completed }
Status public currentStatus;
}
Variables and State
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Variables {
// State variables (stored on blockchain)
uint256 public stateVariable = 100;
function example() public pure returns (uint256) {
// Local variables (stored in memory)
uint256 localVar = 50;
// Local variables are deleted after function execution
return localVar;
}
// Constants (computed at compile time)
uint256 public constant MY_CONSTANT = 1000;
// Immutable (set once in constructor)
address public immutable OWNER;
constructor() {
OWNER = msg.sender;
}
}
Functions
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Functions {
uint256 private count;
// View function - reads state, doesn't modify
function getCount() public view returns (uint256) {
return count;
}
// Pure function - doesn't read or modify state
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
// Function that modifies state
function increment() public {
count += 1;
}
// Payable function - can receive ETH
function deposit() public payable {
// msg.value contains the sent ETH amount
}
// Internal function - only callable within contract
function _internalFunction() internal returns (uint256) {
return count;
}
// External function - only callable from outside
function externalFunction() external returns (uint256) {
return count;
}
}
Control Structures
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ControlStructures {
function exampleIf(uint256 x) public pure returns (string memory) {
if (x > 10) {
return "Greater than 10";
} else if (x == 10) {
return "Equal to 10";
} else {
return "Less than 10";
}
}
function exampleLoop(uint256 n) public pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
function exampleWhile(uint256 n) public pure returns (uint256) {
uint256 sum = 0;
uint256 i = 1;
while (i <= n) {
sum += i;
i++;
}
return sum;
}
}
Arrays and Mappings
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ArraysAndMappings {
// Arrays
uint256[] public dynamicArray;
uint256[10] public fixedArray;
function addToArray(uint256 value) public {
dynamicArray.push(value);
}
function getArrayLength() public view returns (uint256) {
return dynamicArray.length;
}
function removeFromArray() public {
require(dynamicArray.length > 0, "Array is empty");
dynamicArray.pop();
}
// Mappings
mapping(address => uint256) public balances;
mapping(address => mapping(address => bool)) public isApproved;
function setBalance(uint256 amount) public {
balances[msg.sender] = amount;
}
function getBalance(address user) public view returns (uint256) {
return balances[user];
}
}
Structs
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Structs {
struct Book {
string title;
string author;
uint256 pages;
bool isAvailable;
}
Book public myBook;
Book[] public books;
mapping(uint256 => Book) public bookById;
function createBook(
string memory _title,
string memory _author,
uint256 _pages
) public {
// Method 1: Named parameters
myBook = Book({
title: _title,
author: _author,
pages: _pages,
isAvailable: true
});
// Method 2: Positional parameters
books.push(Book(_title, _author, _pages, true));
}
function updateBookAvailability(uint256 index, bool available) public {
books[index].isAvailable = available;
}
}
Error Handling
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ErrorHandling {
uint256 public value;
// require - use for input validation
function setValue(uint256 _value) public {
require(_value > 0, "Value must be greater than 0");
value = _value;
}
// revert - use for complex conditions
function complexValidation(uint256 _value) public {
if (_value == 0) {
revert("Value cannot be zero");
}
if (_value > 100) {
revert("Value cannot exceed 100");
}
value = _value;
}
// assert - use for invariants
function assertExample() public view {
assert(value >= 0); // Should always be true
}
// Custom errors (cheaper gas)
error InsufficientBalance(uint256 available, uint256 required);
function customError(uint256 amount) public view {
if (amount > value) {
revert InsufficientBalance(value, amount);
}
}
}
Events
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Events {
uint256 public count;
event CountChanged(uint256 oldValue, uint256 newValue, address changer);
event Transfer(address indexed from, address indexed to, uint256 amount);
function increment() public {
uint256 oldValue = count;
count += 1;
emit CountChanged(oldValue, count, msg.sender);
}
function transfer(address to, uint256 amount) public {
// ... transfer logic ...
emit Transfer(msg.sender, to, amount);
}
}
Inheritance
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
function transferOwnership(address newOwner) public onlyOwner {
owner = newOwner;
}
}
contract MyContract is Ownable {
uint256 private value;
event ValueChanged(uint256 newValue);
function setValue(uint256 _value) public onlyOwner {
value = _value;
emit ValueChanged(_value);
}
function getValue() public view returns (uint256) {
return value;
}
}
Interface and Abstract Contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Interface - only function signatures
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
}
// Abstract contract - can have implemented functions
abstract contract Animal {
function makeSound() public virtual returns (string memory);
function sleep() public pure returns (string memory) {
return "Zzz...";
}
}
contract Dog is Animal {
function makeSound() public pure override returns (string memory) {
return "Woof!";
}
}
Real-World Example: Simple Token
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleToken {
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals,
uint256 _totalSupply
) {
name = _name;
symbol = _symbol;
decimals = _decimals;
totalSupply = _totalSupply * 10 ** uint256(_decimals);
balanceOf[msg.sender] = totalSupply;
}
function transfer(address to, uint256 value) public returns (bool) {
require(balanceOf[msg.sender] >= value, "Insufficient balance");
require(to != address(0), "Invalid recipient");
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
return true;
}
function approve(address spender, uint256 value) public returns (bool) {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transferFrom(
address from,
address to,
uint256 value
) public returns (bool) {
require(balanceOf[from] >= value, "Insufficient balance");
require(allowance[from][msg.sender] >= value, "Insufficient allowance");
require(to != address(0), "Invalid recipient");
balanceOf[from] -= value;
balanceOf[to] += value;
allowance[from][msg.sender] -= value;
emit Transfer(from, to, value);
return true;
}
}
Gas Optimization Tips
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GasOptimization {
// Use uint256 instead of smaller types (unless packing)
uint256 public value; // Better than uint8 for single variable
// Pack variables in structs
struct Packed {
uint128 a; // 16 bytes
uint128 b; // 16 bytes
// Total: 32 bytes (one storage slot)
}
// Use calldata instead of memory for external functions
function processData(string calldata data) external pure returns (bytes32) {
return keccak256(bytes(data));
}
// Use custom errors instead of strings
error InvalidValue();
function checkValue(uint256 v) public pure {
if (v == 0) revert InvalidValue();
}
// Cache array length in loops
function sumArray(uint256[] memory arr) public pure returns (uint256) {
uint256 total = 0;
uint256 len = arr.length; // Cache length
for (uint256 i = 0; i < len; i++) {
total += arr[i];
}
return total;
}
}
Security Best Practices
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SecurityBestPractices {
address public owner;
mapping(address => uint256) public balances;
// Use ReentrancyGuard pattern
bool private locked;
modifier nonReentrant() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
// Checks-Effects-Interactions pattern
function withdraw(uint256 amount) public nonReentrant {
// 1. Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. Effects
balances[msg.sender] -= amount;
// 3. Interactions
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// Use pull payment pattern
mapping(address => uint256) public pendingWithdrawals;
function depositFor(address user) public payable {
pendingWithdrawals[user] += msg.value;
}
function withdrawPending() public {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No pending withdrawal");
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Conclusion
Solidity is a powerful language for writing smart contracts on Ethereum. By understanding these fundamentals and following best practices, you can create secure and efficient decentralized applications.
Key Takeaways
- Always use the latest stable Solidity version
- Follow the Checks-Effects-Interactions pattern
- Use OpenZeppelin for standard implementations
- Test thoroughly before deploying
- Consider gas optimization
Start building your smart contracts today and join the decentralized future!