Solidity Fundamentals
Module 3 of Ethereum & Smart Contracts
What Is Solidity?
Solidity is the most popular smart contract language:
- Designed specifically for the EVM
- Statically typed
- Object-oriented with inheritance
- Compiles to EVM bytecode
Similar syntax to JavaScript/C++, but with blockchain-specific features.
Basic Structure
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SimpleStorage {
// State variables
uint256 private storedValue;
// Events
event ValueChanged(uint256 newValue);
// Functions
function set(uint256 value) public {
storedValue = value;
emit ValueChanged(value);
}
function get() public view returns (uint256) {
return storedValue;
}
}
Data Types
Value Types
// Integers
uint256 a = 100; // Unsigned, 0 to 2^256-1
int256 b = -50; // Signed, -2^255 to 2^255-1
uint8 c = 255; // Smaller sizes available
// Boolean
bool isActive = true;
// Address
address user = 0x1234...;
address payable recipient = payable(user); // Can receive ETH
// Bytes
bytes32 hash = keccak256("hello");
bytes1 singleByte = 0xff;
Reference Types
// Arrays
uint256[] dynamicArray;
uint256[10] fixedArray;
// Strings
string name = "Ethereum";
// Mappings
mapping(address => uint256) balances;
mapping(address => mapping(address => uint256)) allowances;
// Structs
struct User {
address addr;
uint256 balance;
bool isActive;
}
Functions
Visibility
function publicFunc() public {} // Anyone can call
function externalFunc() external {} // Only external calls
function internalFunc() internal {} // This + derived contracts
function privateFunc() private {} // Only this contract
State Mutability
function readWrite() public {} // Modifies state
function viewOnly() public view {} // Reads state only
function pureCalc() public pure {} // No state access
function receiveETH() public payable {} // Can receive ETH
Function Modifiers
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // Continue with function
}
function sensitiveAction() public onlyOwner {
// Only owner can call
}
Control Structures
// If/Else
if (amount > 100) {
// do something
} else if (amount > 50) {
// do something else
} else {
// default
}
// Loops
for (uint i = 0; i < 10; i++) {
// iterate (be careful of gas!)
}
while (condition) {
// loop while true
}
// Error Handling
require(amount > 0, "Amount must be positive");
assert(totalSupply == expectedSupply);
revert("Something went wrong");
Important Concepts
msg Object
msg.sender // Caller's address
msg.value // ETH sent (in wei)
msg.data // Complete calldata
Ether Units
1 wei = 1
1 gwei = 10^9 wei
1 ether = 10^18 wei
require(msg.value >= 1 ether, "Send at least 1 ETH");
Events
event Transfer(
address indexed from,
address indexed to,
uint256 value
);
function transfer(address to, uint256 amount) public {
// ... logic ...
emit Transfer(msg.sender, to, amount);
}
- Indexed parameters are searchable
- Events are cheap (stored in logs, not state)
- Essential for dApp frontends
Inheritance
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
}
contract MyContract is Ownable {
// Inherits owner and onlyOwner
function adminOnly() public onlyOwner {
// Protected function
}
}
Multiple Inheritance
contract Child is Parent1, Parent2 {
// Inherits from both
// Order matters for constructor calls
}
Interfaces
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract MyContract {
function sendTokens(address token, address to, uint256 amount) external {
IERC20(token).transfer(to, amount);
}
}
Common Patterns
Checks-Effects-Interactions
function withdraw(uint256 amount) public {
// CHECKS
require(balances[msg.sender] >= amount, "Insufficient");
// EFFECTS
balances[msg.sender] -= amount;
// INTERACTIONS
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
Pull Over Push
// BAD: Push payments
function distribute() public {
for (uint i = 0; i < users.length; i++) {
payable(users[i]).transfer(amounts[i]); // Can fail!
}
}
// GOOD: Pull payments
mapping(address => uint256) public pendingWithdrawals;
function withdraw() public {
uint256 amount = pendingWithdrawals[msg.sender];
pendingWithdrawals[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
Gas Optimization Tips
- Use calldata for external function arrays
function process(uint256[] calldata data) external {} // Cheaper
- Pack storage variables
// Uses 2 slots
uint256 a;
uint128 b;
uint128 c;
// Uses 2 slots (packed)
uint128 b;
uint128 c; // Packed with b
uint256 a;
- Use immutable for constants
address public immutable WETH; // Set once in constructor, no SLOAD
- Cache storage reads
uint256 cachedBalance = balances[user];
// Use cachedBalance multiple times
Security Best Practices
- Use latest Solidity (0.8+ has overflow protection)
- Check return values of external calls
- Use ReentrancyGuard for state-changing functions
- Validate all inputs
- Use OpenZeppelin battle-tested contracts
- Get audited before mainnet
Key Takeaways
- Solidity is EVM-specific — different from web development
- State changes cost gas — optimize storage
- Security is critical — code handles real money
- Events are essential — for off-chain tracking
- Inheritance and interfaces enable composability
- Follow patterns — Checks-Effects-Interactions, etc.
Questions to Consider
- Why is there no floating-point in Solidity?
- When should you use memory vs calldata?
- What's the cost difference between storage and memory?
- How do you test Solidity contracts?