Learning Objectives:
-
Master Solidity programming language fundamentals
-
Understand data types, variables, and control structures
-
Learn function declarations, modifiers, and error handling
-
Analyze contract structure, inheritance, and libraries
4.2.1: Introduction to Solidity
What is Solidity?
Solidity is a high-level, statically-typed, object-oriented programming language designed for writing smart contracts on Ethereum and other EVM-compatible blockchains.
Solidity Overview: ┌─────────────────────────────────────────────────────────────────────┐ │ Solidity Characteristics │ │ │ │ • High-level language │ │ • Statically typed │ │ • Object-oriented │ │ • Designed for EVM │ │ • Influenced by C++, Python, JavaScript │ │ • File extension: .sol │ │ • Compiles to EVM bytecode │ │ │ └─────────────────────────────────────────────────────────────────────┘
Solidity Versioning:
Solidity uses semantic versioning and requires explicit version declarations.
Version Declaration: pragma solidity ^0.8.0; // ^0.8.0: Any version >=0.8.0 and <0.9.0 pragma solidity >=0.8.0 <0.9.0; // Explicit range pragma solidity 0.8.20; // Exact version Version Numbers: - Major: Breaking changes (0.9.0) - Minor: New features (0.8.20) - Patch: Bug fixes (0.8.20)
Development Environment:
Tools and Frameworks: 1. Remix IDE: - Browser-based IDE - Quick prototyping - Built-in compiler - Deployment tools 2. Hardhat: - Node.js framework - Testing environment - Plugin system - Debugging tools 3. Foundry: - Rust-based framework - Fast testing - Built-in fuzzing - Command-line tools 4. Truffle: - Suite development - Migration tools - Testing framework - Network management
4.2.2: Basic Solidity Syntax
Contract Structure:
Basic Contract Structure:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyContract {
// State variables
uint256 public myNumber;
address public owner;
// Events
event NumberUpdated(uint256 newNumber);
// Modifiers
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
// Constructor
constructor() {
owner = msg.sender;
}
// Functions
function setNumber(uint256 _number) public onlyOwner {
myNumber = _number;
emit NumberUpdated(_number);
}
function getNumber() public view returns (uint256) {
return myNumber;
}
}
Comments:
Comment Types: // Single-line comment /* Multi-line comment Can span multiple lines */ /// Documentation comment (NatSpec) /// @param _number The new number to set /// @return The current number
4.2.3: Data Types
Value Types:
| Type | Description | Example |
|---|---|---|
| bool | Boolean value | true, false |
| int | Signed integer | -10, 0, 5 |
| uint | Unsigned integer | 0, 5, 10 |
| address | Ethereum address | 0x742d… |
| address payable | Address that can receive Ether | 0x742d… |
| bytes | Dynamic byte array | “Hello” |
| bytes1-bytes32 | Fixed byte array | bytes32 |
| string | UTF-8 string | “Hello” |
| enum | User-defined enumeration | Status {Pending, Active} |
| fixed/ufixed | Fixed-point numbers (deprecated) | – |
Integer Types:
Integer Ranges: uint8: 0 to 2^8 - 1 (0 to 255) uint16: 0 to 2^16 - 1 uint32: 0 to 2^32 - 1 uint64: 0 to 2^64 - 1 uint128: 0 to 2^128 - 1 uint256: 0 to 2^256 - 1 (default) int8: -2^7 to 2^7 - 1 int16: -2^15 to 2^15 - 1 int32: -2^31 to 2^31 - 1 int64: -2^63 to 2^63 - 1 int128: -2^127 to 2^127 - 1 int256: -2^255 to 2^255 - 1 (default) Gas Optimization: - Use smaller types when possible - Use uint256 for arithmetic (more efficient) - Pack multiple variables in storage
Address Types:
Address Operations:
address addr = 0x742d35Cc6634C0532925a3b844Bc454e4438f44e;
// Send Ether
addr.transfer(amount); // 2300 gas, throws on failure
(bool success, ) = addr.call{value: amount}(""); // 63/64 gas forward
// Check balance
uint256 balance = addr.balance;
// Call function
(bool success, bytes memory data) = addr.call(abi.encodeWithSignature("func()"));
// Address payable (can receive Ether)
address payable payableAddr = payable(addr);
String and Bytes:
String vs Bytes: string: UTF-8 encoded, dynamic length bytes: Raw byte array, dynamic length bytes32: Fixed-length byte array (32 bytes) String Operations: string greeting = "Hello, World!"; string concatenated = string.concat(greeting, " Welcome!"); Bytes Operations: bytes memory data = hex"48656c6c6f"; // "Hello" uint256 length = data.length;
Arrays:
Array Types: // Fixed-size array uint256[5] fixedArray; // Dynamic array uint256[] dynamicArray; // Memory array uint256[] memory memArray = new uint256[](5); // Array methods dynamicArray.push(10); // Add element dynamicArray.pop(); // Remove last uint256 length = dynamicArray.length; // Access uint256 first = dynamicArray[0];
Structs:
Struct Definition:
struct Person {
string name;
uint256 age;
address wallet;
}
// Usage
Person[] public people;
Person memory newPerson = Person("Alice", 30, 0x123...);
// Assign
people.push(newPerson);
people.push(Person("Bob", 25, 0x456...));
// Access
Person storage person = people[0];
string name = person.name;
Mappings:
Mapping Definition: mapping(address => uint256) public balances; mapping(address => mapping(address => uint256)) public allowances; // Usage balances[msg.sender] = 1000; uint256 balance = balances[msg.sender]; // Mapping is like a dictionary // Keys can be any type except complex types // Values can be any type // Not iterable (no length or enumeration)
Enums:
Enum Definition:
enum Status {
Pending,
Active,
Inactive,
Completed
}
// Usage
Status public currentStatus = Status.Pending;
function setActive() public {
currentStatus = Status.Active;
}
function isPending() public view returns (bool) {
return currentStatus == Status.Pending;
}
4.2.4: Variables and Scope
Variable Types:
Variable Types: 1. State Variables: - Stored on blockchain - Persistent between function calls - Expensive to modify 2. Local Variables: - Temporary within function - Stored in memory - No persistent storage 3. Global Variables: - Available in all functions - Provide blockchain data - msg, block, tx, etc. 4. Constants: - Immutable at compile-time - Not stored in storage - Lower gas cost 5. Immutable Variables: - Set at construction time - Cannot be changed - Lower gas cost
Global Variables:
Global Variables (msg): msg.sender: Caller address msg.value: Amount of Ether sent msg.data: Complete calldata msg.sig: Function signature (first 4 bytes) Global Variables (block): block.timestamp: Current block timestamp block.number: Current block number block.difficulty: Current block difficulty block.coinbase: Block validator address block.gaslimit: Block gas limit block.chainid: Current chain ID Global Variables (tx): tx.gasprice: Transaction gas price tx.origin: Original sender (can be different from msg.sender) Other: gasleft(): Remaining gas now: Alias for block.timestamp
4.2.5: Functions
Function Types:
Function Declaration:
function functionName(
// Parameters
uint256 param1,
address param2
)
// Visibility
public
// Mutability
view
// Returns
returns (uint256, address)
{
// Function body
}
Visibility:
- public: Accessible from anywhere
- private: Only within contract
- internal: Within contract and derived contracts
- external: Only from external calls
Mutability Modifiers:
| Modifier | Description | Gas Cost |
|---|---|---|
| view | Reads state, no modification | Low |
| pure | No state access, no modification | Low |
| payable | Can receive Ether | Higher |
| nonpayable | Cannot receive Ether | Default |
| default | Can modify state | High |
Function Examples:
View Function:
function getBalance() public view returns (uint256) {
return address(this).balance;
}
Pure Function:
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
Payable Function:
function deposit() public payable {
// msg.value available
emit Deposit(msg.sender, msg.value);
}
Multiple Returns:
function getData() public view returns (uint256, address, bool) {
return (123, msg.sender, true);
}
4.2.6: Modifiers
Modifier Definition:
Modifier Structure:
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // Function body executes here
}
modifier validAddress(address _addr) {
require(_addr != address(0), "Invalid address");
_;
}
modifier whenNotPaused() {
require(!paused, "Contract paused");
_;
}
Modifier Usage:
Using Modifiers:
contract MyContract {
address public owner;
bool public paused;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier whenNotPaused() {
require(!paused, "Paused");
_;
}
// Function with modifiers
function withdraw(uint256 amount)
public
onlyOwner
whenNotPaused
{
// Function body
}
// Modifier with parameters
modifier minimumAmount(uint256 minAmount) {
require(msg.value >= minAmount, "Amount too low");
_;
}
}
4.2.7: Error Handling
Error Types:
Error Handling Methods: 1. require(): - Check conditions - Refund remaining gas - Use for input validation 2. assert(): - Check invariants - Consume all gas on failure - Use for internal errors 3. revert(): - Manual revert - Refund remaining gas - Use for complex conditions 4. Custom Errors (Solidity 0.8.4+): - More gas efficient - Better debugging - Use for common errors
Implementation:
Error Handling Examples:
// Using require
function setValue(uint256 _value) public {
require(_value > 0, "Value must be positive");
value = _value;
}
// Using assert
function withdraw(uint256 amount) public {
uint256 balance = balances[msg.sender];
assert(balance >= amount);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
// Using revert
function transfer(address to, uint256 amount) public {
if (amount > balances[msg.sender]) {
revert("Insufficient balance");
}
// Transfer logic
}
// Custom errors
error InsufficientBalance(uint256 requested, uint256 available);
error Unauthorized(address caller);
function withdraw(uint256 amount) public {
if (amount > balances[msg.sender]) {
revert InsufficientBalance({
requested: amount,
available: balances[msg.sender]
});
}
// Transfer logic
}
4.2.8: Events
Event Definition:
Event Declaration:
event Transfer(address indexed from, address indexed to, uint256 value);
Event Fields:
- indexed: Up to 3 indexed fields
- Indexed fields can be filtered
- Non-indexed fields stored in data
Event Example:
contract ERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function transfer(address to, uint256 value) public {
// Logic
emit Transfer(msg.sender, to, value);
}
}
4.2.9: Inheritance
Inheritance Syntax:
Single Inheritance:
contract Base {
uint256 public data;
function setData(uint256 _data) public {
data = _data;
}
}
contract Derived is Base {
function getData() public view returns (uint256) {
return data;
}
}
Multiple Inheritance:
Multiple Inheritance:
contract A {
function foo() public virtual pure returns (string memory) {
return "A";
}
}
contract B {
function foo() public virtual pure returns (string memory) {
return "B";
}
}
contract C is A, B {
// Inheritance order: A then B
// C.foo() returns "A"
function foo() public override(A, B) pure returns (string memory) {
return super.foo(); // Returns "A"
}
}
Super Keyword:
Using Super:
contract Parent {
uint256 public value;
function setValue(uint256 _value) public virtual {
value = _value;
}
}
contract Child is Parent {
function setValue(uint256 _value) public override {
// Additional logic
require(_value > 0, "Must be positive");
super.setValue(_value); // Call parent
}
}
4.2.10: Libraries
Library Definition:
Library Syntax:
library Math {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
return a + b;
}
function subtract(uint256 a, uint256 b) internal pure returns (uint256) {
require(a >= b, "Subtraction overflow");
return a - b;
}
}
Using Libraries:
Library Usage:
contract MyContract {
using Math for uint256;
function calculate(uint256 a, uint256 b) public pure returns (uint256) {
return a.add(b); // Using library method
}
}
ADDITIONAL DEEP TECHNICAL NOTES:
1. Solidity Assembly (Yul)
Assembly Usage:
function assemblyExample() public pure returns (uint256) {
uint256 result;
assembly {
// Yul assembly
let x := 10
let y := 20
result := add(x, y)
// Memory operations
let free := mload(0x40)
mstore(free, 0x12345678)
result := mload(free)
}
return result;
}
2. Gas Optimization Techniques
Gas Optimization Tips:
1. Use uint256 for arithmetic:
- Cheaper for calculations
- Default type is uint256
2. Pack storage variables:
- uint8, uint16, uint32, etc.
- Max of 32 bytes per slot
3. Use short-circuit evaluation:
- if (conditionA && conditionB) stops early
4. Use view/pure when possible:
- No gas for reading
5. Use custom errors (0.8.4+):
- Cheaper than require strings
6. Use events for logs:
- Events are cheaper than storage
7. Avoid loops when possible:
- Loops cost gas per iteration
8. Use external functions:
- Cheaper than public (calldata vs memory)
9. Use calldata for read-only data:
- Cheaper than memory
10. Use immutable variables:
- Cheaper than storage
3. Common Solidity Patterns
Common Patterns:
1. Withdrawal Pattern:
function withdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
balances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
2. Checks-Effects-Interactions:
function withdraw() public {
// Checks
require(amount > 0, "Invalid amount");
// Effects
balances[msg.sender] -= amount;
// Interactions
payable(msg.sender).transfer(amount);
}
3. Restrict Access:
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
4. Emergency Stop:
modifier whenNotPaused() {
require(!paused, "Paused");
_;
}
4. Solidity Security Considerations
Security Best Practices: 1. Reentrancy Protection: - Use checks-effects-interactions - Use reentrancy guard 2. Integer Overflow/Underflow: - Use SafeMath (pre-0.8.0) - Solidity 0.8.0+ has built-in checks 3. Access Control: - Use modifiers - Use Ownable pattern 4. Gas Limits: - Avoid unbounded loops - Use pagination 5. Randomness: - Use chainlink VRF - Avoid block hash 6. Front-running: - Use commit-reveal pattern - Use time locks 7. Denial of Service: - Avoid loops with external calls - Use withdrawal pattern