Master Solidity
From Newbie to Professional
Build Your Blockchain Development Career
Authored by William H. Simmons
Founder of A Few Bad Newbies LLC
Solidity Professional Development Course
Module 1: Introduction to Blockchain and Solidity
Chapter 1: Understanding Blockchain
Blockchain is a decentralized, immutable ledger that records transactions across a network of computers. Ethereum is a leading blockchain platform supporting smart contracts.
- Decentralized: No single authority controls the network.
- Immutable: Once recorded, data cannot be altered.
- Smart Contracts: Self-executing contracts with coded terms.
Pro Tip
Think of blockchain as a shared, tamper-proof notebook where everyone has a copy.
Practice exploring blockchain:
// Research Ethereum testnets like Sepolia
const ethers = require("ethers");
const provider = new ethers.providers.JsonRpcProvider("https://sepolia.infura.io/v3/YOUR-API-KEY");
Chapter 2: Introduction to Solidity
Solidity is a high-level, object-oriented language for writing smart contracts on Ethereum. It’s statically typed and supports inheritance.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloWorld {
string public greeting = "Hello, Blockchain!";
function setGreeting(string memory _greeting) public {
greeting = _greeting;
}
}
Common Mistakes
- Forgetting the SPDX license identifier.
- Not specifying the Solidity version with
pragma.
Practice a simple contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Test {
string public message = "Hi";
}
Chapter 3: Setting Up Your Environment
To develop Solidity smart contracts, set up tools like Remix IDE, Hardhat, and MetaMask.
// Install Hardhat
npm install --save-dev hardhat
// Initialize a Hardhat project
npx hardhat
Pro Tip
Use Remix for quick prototyping and Hardhat for production-grade development.
Practice setting up:
// Install MetaMask browser extension and connect to a testnet
const provider = new ethers.providers.Web3Provider(window.ethereum);
Module 2: Solidity Basics
Chapter 1: Variables and Data Types
Solidity supports types like uint, address, and string.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DataTypes {
uint count = 100;
address owner = msg.sender;
string name = "Solidity";
function getValues() public view returns (uint, address, string memory) {
return (count, owner, name);
}
}
Pro Tip
Use uint for non-negative numbers to save gas.
Practice variables:
pragma solidity ^0.8.0;
contract Test {
uint value = 10;
}
Chapter 2: Functions and Visibility
Functions define logic with visibility specifiers like public or view.
pragma solidity ^0.8.0;
contract Functions {
uint public value;
function setValue(uint _value) public {
value = _value;
}
function getValue() public view returns (uint) {
return value;
}
}
Common Mistakes
- Not specifying visibility, defaulting to
public. - Using
viewfor state-modifying functions.
Practice a function:
pragma solidity ^0.8.0;
contract Test {
function getOne() public pure returns (uint) {
return 1;
}
}
Chapter 3: Control Structures
Control structures like if and for manage logic flow.
pragma solidity ^0.8.0;
contract Control {
function isEven(uint x) public pure returns (bool) {
if (x % 2 == 0) {
return true;
}
return false;
}
function sum(uint n) public pure returns (uint) {
uint total = 0;
for (uint i = 1; i <= n; i++) {
total += i;
}
return total;
}
}
Common Mistakes
- Unbounded loops causing gas limit errors.
- Incorrect conditional logic leading to reverts.
Practice a conditional:
pragma solidity ^0.8.0;
contract Test {
function check(uint x) public pure returns (bool) {
if (x > 0) {
return true;
}
return false;
}
}
Module 3: Data Structures
Chapter 1: Arrays
Arrays store ordered collections, either fixed or dynamic.
pragma solidity ^0.8.0;
contract Arrays {
uint[] public numbers;
function addNumber(uint _num) public {
numbers.push(_num);
}
function getLength() public view returns (uint) {
return numbers.length;
}
}
Pro Tip
Use fixed-size arrays when possible to save gas.
Practice an array:
pragma solidity ^0.8.0;
contract Test {
uint[] arr;
function add(uint x) public {
arr.push(x);
}
}
Chapter 2: Mappings
Mappings are key-value stores for efficient data access.
pragma solidity ^0.8.0;
contract Mappings {
mapping(address => uint) public balances;
function setBalance(uint _amount) public {
balances[msg.sender] = _amount;
}
}
Common Mistakes
- Attempting to iterate over mappings (not supported).
- Not handling default values (e.g., 0 for uint).
Practice a mapping:
pragma solidity ^0.8.0;
contract Test {
mapping(uint => bool) flags;
function setFlag(uint key) public {
flags[key] = true;
}
}
Chapter 3: Structs and Enums
Structs group related data; enums define fixed states.
pragma solidity ^0.8.0;
contract StructsEnums {
enum Status { Pending, Active, Inactive }
struct User {
address addr;
uint balance;
Status status;
}
User[] public users;
function addUser(address _addr, uint _balance) public {
users.push(User(_addr, _balance, Status.Pending));
}
}
Pro Tip
Pack struct fields (e.g., use uint8) to optimize storage.
Practice a struct:
pragma solidity ^0.8.0;
contract Test {
struct Data {
uint value;
}
Data data;
function setData(uint v) public {
data = Data(v);
}
}
Module 4: Contract Interactions
Chapter 1: Inheritance
Inheritance enables code reuse across contracts.
pragma solidity ^0.8.0;
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
}
contract MyContract is Ownable {
function restricted() public onlyOwner {
// Restricted logic
}
}
Pro Tip
Use OpenZeppelin’s Ownable for secure ownership patterns.
Practice inheritance:
pragma solidity ^0.8.0;
contract Base {
uint x;
}
contract Derived is Base {}
Chapter 2: Interfaces
Interfaces define function signatures for external contract calls.
pragma solidity ^0.8.0;
interface IERC20 {
function transfer(address to, uint amount) external returns (bool);
}
contract TokenConsumer {
function sendToken(address token, address to, uint amount) public {
IERC20(token).transfer(to, amount);
}
}
Common Mistakes
- Not matching interface signatures exactly.
- Calling unverified contract addresses.
Practice an interface:
pragma solidity ^0.8.0;
interface ITest {
function test() external;
}
Chapter 3: Payable Functions
Payable functions receive Ether with transactions.
pragma solidity ^0.8.0;
contract Payments {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function getBalance() public view returns (uint) {
return balances[msg.sender];
}
}
Pro Tip
Use payable only when necessary to limit Ether handling.
Practice a payable function:
pragma solidity ^0.8.0;
contract Test {
function pay() public payable {}
}
Module 5: Security and Error Handling
Chapter 1: Error Handling
Use require, assert, and custom errors for robust error handling.
pragma solidity ^0.8.0;
contract Errors {
error InsufficientBalance(uint available, uint required);
mapping(address => uint) balances;
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount, "Not enough funds");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
Common Mistakes
- Using string messages instead of custom errors, increasing gas costs.
- Not validating inputs with
require.
Practice error handling:
pragma solidity ^0.8.0;
contract Test {
function check(uint x) public pure {
require(x > 0, "Must be positive");
}
}
Chapter 2: Security Best Practices
Protect contracts from vulnerabilities like reentrancy.
pragma solidity ^0.8.0;
contract Secure {
mapping(address => uint) balances;
bool locked;
modifier noReentrancy() {
require(!locked, "Reentrancy detected");
locked = true;
_;
locked = false;
}
function withdraw(uint amount) public noReentrancy {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
Pro Tip
Update state before external calls to prevent reentrancy.
Practice a secure function:
pragma solidity ^0.8.0;
contract Test {
mapping(address => uint) balances;
function setBalance(uint amount) public {
balances[msg.sender] = amount;
}
}
Chapter 3: Gas Optimization
Optimize contracts to reduce gas costs.
pragma solidity ^0.8.0;
contract Optimization {
uint8 a;
uint8 b;
uint immutable limit = 100;
function add(uint x, uint y) public pure returns (uint) {
unchecked { return x + y; }
}
}
Common Mistakes
- Using large data types like
uint256unnecessarily. - Not using
immutablefor constants set at deployment.
Practice gas optimization:
pragma solidity ^0.8.0;
contract Test {
uint8 value;
function setValue(uint8 v) public {
value = v;
}
}
Module 6: Events and Modifiers
Chapter 1: Events
Events log data to the blockchain for external applications.
pragma solidity ^0.8.0;
contract Events {
event ValueSet(address indexed sender, uint value);
function setValue(uint _value) public {
emit ValueSet(msg.sender, _value);
}
}
Pro Tip
Use indexed parameters for efficient filtering in event logs.
Practice an event:
pragma solidity ^0.8.0;
contract Test {
event Log(uint value);
function log(uint x) public {
emit Log(x);
}
}
Chapter 2: Function Modifiers
Modifiers add reusable logic to functions.
pragma solidity ^0.8.0;
contract Modifiers {
address public owner = msg.sender;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function restricted() public onlyOwner {
// Restricted logic
}
}
Common Mistakes
- Forgetting the
_;placeholder in modifiers. - Overcomplicating modifier logic, causing errors.
Practice a modifier:
pragma solidity ^0.8.0;
contract Test {
modifier check() {
require(true);
_;
}
function test() public check {}
}
Chapter 3: Fallback and Receive Functions
Fallback and receive functions handle unexpected calls or Ether.
pragma solidity ^0.8.0;
contract Fallback {
event Received(address sender, uint amount);
receive() external payable {
emit Received(msg.sender, msg.value);
}
fallback() external payable {
emit Received(msg.sender, msg.value);
}
}
Pro Tip
Keep fallback functions simple to avoid high gas costs.
Practice a receive function:
pragma solidity ^0.8.0;
contract Test {
receive() external payable {}
}
Module 7: Token Standards
Chapter 1: ERC-20 Tokens
ERC-20 is the standard for fungible tokens.
pragma solidity ^0.8.0;
contract MyToken {
string public name = "My Token";
string public symbol = "MTK";
uint public totalSupply;
mapping(address => uint) public balances;
event Transfer(address indexed from, address indexed to, uint amount);
function transfer(address to, uint amount) public returns (bool) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}
Common Mistakes
- Not emitting required
Transferevents. - Ignoring safe math for arithmetic operations.
Practice an ERC-20 function:
pragma solidity ^0.8.0;
contract Test {
mapping(address => uint) balances;
function setBalance(address to, uint amount) public {
balances[to] = amount;
}
}
Chapter 2: ERC-721 NFTs
ERC-721 defines non-fungible tokens for unique assets.
pragma solidity ^0.8.0;
contract MyNFT {
mapping(uint => address) public owners;
uint tokenId;
event Transfer(address indexed from, address indexed to, uint tokenId);
function mint(address to) public {
owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
tokenId++;
}
}
Pro Tip
Use OpenZeppelin’s ERC-721 for secure NFT implementations.
Practice an NFT mint function:
pragma solidity ^0.8.0;
contract Test {
mapping(uint => address) owners;
function mint(address to, uint id) public {
owners[id] = to;
}
}
Chapter 3: Safe Math
Safe math prevents arithmetic overflows and underflows.
pragma solidity ^0.8.0;
contract SafeMath {
function add(uint a, uint b) public pure returns (uint) {
uint c = a + b;
require(c >= a, "Overflow");
return c;
}
}
Common Mistakes
- Not checking for overflows in older Solidity versions.
- Reimplementing safe math instead of using libraries.
Practice safe math:
pragma solidity ^0.8.0;
contract Test {
function safeAdd(uint a, uint b) public pure returns (uint) {
uint c = a + b;
require(c >= a, "Overflow");
return c;
}
}
Module 8: Smart Contracts
Chapter 1: Writing Smart Contracts
Smart contracts are self-executing programs that enforce agreements on the blockchain.
pragma solidity ^0.8.0;
contract Voting {
mapping(address => bool) public hasVoted;
mapping(uint => uint) public votes;
event Voted(address indexed voter, uint candidate);
function vote(uint candidate) public {
require(!hasVoted[msg.sender], "Already voted");
hasVoted[msg.sender] = true;
votes[candidate] += 1;
emit Voted(msg.sender, candidate);
}
}
Pro Tip
Structure contracts with clear state and logic to ensure transparency.
Practice a simple contract:
pragma solidity ^0.8.0;
contract Test {
uint public counter;
function increment() public {
counter += 1;
}
}
Chapter 2: Interacting with Smart Contracts
Interact with contracts using interfaces or low-level calls.
pragma solidity ^0.8.0;
interface IVoting {
function vote(uint candidate) external;
}
contract Caller {
function callVote(address votingContract, uint candidate) public {
IVoting(votingContract).vote(candidate);
}
function lowLevelCall(address target, uint candidate) public {
(bool success, ) = target.call(abi.encodeWithSignature("vote(uint256)", candidate));
require(success, "Call failed");
}
}
Common Mistakes
- Not checking success of low-level calls.
- Using incorrect function signatures in ABI encoding.
Practice a contract call:
pragma solidity ^0.8.0;
contract Test {
function callOther(address target) public {
(bool success, ) = target.call("");
require(success);
}
}
Chapter 3: Security Best Practices
Secure smart contracts with checks-effects-interactions and reentrancy guards.
pragma solidity ^0.8.0;
contract SecureVault {
mapping(address => uint) public balances;
bool locked;
modifier noReentrancy() {
require(!locked, "Reentrancy detected");
locked = true;
_;
locked = false;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint amount) public noReentrancy {
require(balances[msg.sender] >= amount, "Insufficient funds");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
Pro Tip
Audit contracts with tools like Slither to catch vulnerabilities.
Practice a secure withdraw:
pragma solidity ^0.8.0;
contract Test {
mapping(address => uint) balances;
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
}
}
Module 9: Advanced Contract Features
Chapter 1: Libraries
Libraries provide reusable code without deployment costs.
pragma solidity ^0.8.0;
library Math {
function max(uint a, uint b) internal pure returns (uint) {
return a > b ? a : b;
}
}
contract Calculator {
using Math for uint;
function getMax(uint a, uint b) public pure returns (uint) {
return a.max(b);
}
}
Pro Tip
Use OpenZeppelin libraries for secure, tested code.
Practice a library:
pragma solidity ^0.8.0;
library Test {
function inc(uint x) internal pure returns (uint) {
return x + 1;
}
}
Chapter 2: Upgradable Contracts
Proxy patterns allow contract upgrades without changing the address.
pragma solidity ^0.8.0;
contract Proxy {
address public implementation;
function upgrade(address newImpl) public {
implementation = newImpl;
}
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "Call failed");
}
}
Common Mistakes
- Not initializing storage in proxies correctly.
- Using
delegatecallwithout safety checks.
Practice a proxy setup:
pragma solidity ^0.8.0;
contract Test {
address implementation;
function setImpl(address impl) public {
implementation = impl;
}
}
Chapter 3: Cross-Contract Calls
Contracts can call each other using interfaces or low-level calls.
pragma solidity ^0.8.0;
contract Target {
uint public value;
function setValue(uint _value) public {
value = _value;
}
}
contract Caller {
function callTarget(address target, uint value) public {
(bool success, ) = target.call(abi.encodeWithSignature("setValue(uint256)", value));
require(success, "Call failed");
}
}
Pro Tip
Prefer interfaces over low-level calls for type safety.
Practice a cross-contract call:
pragma solidity ^0.8.0;
contract Test {
function callOther(address target) public {
(bool success, ) = target.call("");
require(success);
}
}
Module 10: Building and Deploying
Chapter 1: Testing Smart Contracts
Testing ensures contract reliability using tools like Hardhat.
<-foundry>// Hardhat test example const { expect } = require("chai"); describe("MyContract", function() { it("should set value correctly", async function() { const MyContract = await ethers.getContractFactory("MyContract"); const contract = await MyContract.deploy(); await contract.setValue(100); expect(await contract.getValue()).to.equal(100); }); });
Common Mistakes
- Not testing edge cases like zero values.
- Ignoring gas costs in test scenarios.
Practice a test:
describe("Test", function() {
it("should return 1", async function() {
const Test = await ethers.getContractFactory("Test");
const contract = await Test.deploy();
expect(await contract.getOne()).to.equal(1);
});
});
Chapter 2: Deploying to Ethereum
Deploy contracts using Hardhat or Remix to testnets or mainnet.
// Hardhat deployment script
async function main() {
const MyContract = await ethers.getContractFactory("MyContract");
const contract = await MyContract.deploy();
console.log("Contract deployed to:", contract.address);
}
main().catch(console.error);
Pro Tip
Test on networks like Sepolia before deploying to mainnet.
Practice a deployment:
async function deploy() {
const Test = await ethers.getContractFactory("Test");
const contract = await Test.deploy();
}
Chapter 3: Interacting with Deployed Contracts
Use JavaScript libraries like ethers.js to interact with deployed contracts.
const ethers = require("ethers");
async function interact(contractAddress) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const contract = new ethers.Contract(contractAddress, abi, signer);
await contract.setValue(100);
}
Common Mistakes
- Using incorrect ABI for the contract.
- Not connecting to the correct network.
Practice contract interaction:
async function call(address) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
}
Solidity Career Paths
Complete all modules and pass the final test!