solidity

Master Solidity – From Beginner to Professional
EVM

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 view for 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 uint256 unnecessarily.
  • Not using immutable for 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 Transfer events.
  • 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 delegatecall without 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

Smart Contract Developer

Write and audit secure smart contracts for blockchain applications.

Salary Range: $80,000 – $180,000

DeFi Developer

Build decentralized finance protocols like lending platforms and DEXs.

Salary Range: $100,000 – $250,000

Blockchain Engineer

Develop decentralized applications and blockchain infrastructure.

Salary Range: $90,000 – $200,000

Freelance Developer

Work independently building smart contracts for clients.

Earnings Potential: $75,000 – $300,000+

Complete all modules and pass the final test!

Airdrop Points: 0

© 2025 A Few Bad Newbies LLC. All rights reserved.

Note: This course provides educational content but does not offer official certifications or licenses.