Welcome to Solidity Mondays! This is a structured weekly plan to help you learn Solidity, the programming language used to write smart contracts on Ethereum and EVM compatible chains. Each week, weβll cover a new topic, gradually building your skills from beginner to advanced.
Week 1: Introduction to Solidity and Blockchain Basics
- Evolution of the Web: Web1, Web2, and Web3
- Overview of blockchain and Ethereum.
- Smart contracts: What they are and why they matter.
- Setting up your development environment (Remix IDE, MetaMask, and Node.js).
This lesson covers the evolution of the web (Web1, Web2, Web3), blockchain fundamentals, wallets, Ethereum smart contracts, and Solidity programming.
- Read-only web where users could only consume content.
- Static websites with minimal interaction.
- Examples: Yahoo, early blogs, and company websites.
- Read and write capabilities, allowing user-generated content.
- Centralized platforms control data (Facebook, Google, Twitter).
- Monetization through ads and data collection.
- Problems: Privacy issues, censorship, platform dependence.
- Built on blockchain and smart contracts.
- Users own their data, assets, and identities.
- Peer-to-peer interactions without intermediaries.
- Examples: Ethereum-based DApps, DAOs, DeFi, NFTs.
Blockchain is a decentralized, distributed ledger that records transactions securely and transparently.
- Decentralization β No central authority.
- Transparency β Publicly accessible transactions.
- Security β Cryptographic encryption ensures integrity.
- Immutability β Transactions cannot be altered once confirmed.
- Public Blockchains (Ethereum, Bitcoin) β Open networks, permissionless access.
- Private Blockchains (Hyperledger) β Restricted access for enterprises.
- Consortium Blockchains β Controlled by multiple entities.
A crypto wallet allows users to store, send, and receive digital assets.
- Custodial Wallets β Centralized control (e.g., Binance, Coinbase).
- Non-Custodial Wallets β User-controlled keys (e.g., MetaMask, Trust Wallet).
- Hardware Wallets β Secure offline storage (e.g., Ledger, Trezor).
- Wallet Address β Public identifier for receiving funds.
- Private Key β Secret code controlling wallet access.
- A decentralized smart contract platform.
- Uses Ether (ETH) as the native cryptocurrency.
- Supports ERC20 (tokens) and ERC721 (NFTs).
- Decentralized Finance (DeFi) β Lending, borrowing, staking.
- NFTs β Digital ownership of assets.
- DAOs β Community-driven governance.
A high-level language for writing smart contracts on Ethereum, similar to JavaScript and Python.
- Open Remix.
- Create a new Solidity file (
.sol). - Compile and deploy using MetaMask.
- Book: Mastering Ethereum by Andreas M. Antonopoulos and Gavin Wood (Chapter 1: Introduction to Ethereum).
- Online Resources: Solidity Documentation.
- Tools: Install Remix IDE and MetaMask.
Week 2: Solidity Fundamentals
WEEK 2 PRESENTATION
REMIX PRESENTATION
SEPOLIA FAUCET
https://cloud.google.com/application/web3/faucet/ethereum/sepolia
- Basic syntax and structure of a Solidity contract.
- Data types:
uint,address,bool,string, etc. - Variables: State variables, local variables, and constants.
- Functions: Visibility (
public,private,internal,external), and modifiers.
A Solidity smart contract starts with the pragma directive, followed by the contract definition. Solidity contracts contain functions, variables, and logic that define how they interact on the blockchain.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; // Specifies the Solidity version
contract MyFirstContract { // Contract content goes here }
- Boolean (
bool): Storestrueorfalse. - Unsigned Integer (
uint): Represents non-negative integers. - Signed Integer (
int): Stores positive and negative integers. - Address (
address): Stores Ethereum addresses. - Bytes (
bytes1tobytes32): Used for cryptographic operations. - String (
string): Used for storing text.
Functions define the behavior of a smart contract. They can be public, private, view (read-only), or payable (can receive Ether).
function getName() public pure returns (string memory) {
return "Solidity Mondays"; // Returns a fixed string
}
State variables are permanently stored on the blockchain. They retain their values even after the contract execution ends.
contract Example {
uint256 public storedNumber; // A state variable stored on the blockchain
function setNumber(uint256 _num) public {
storedNumber = _num; // Updates the state variable
}
}
Local variables exist only within a function's execution scope. They do not persist on the blockchain.
function getNumber() public pure returns (uint256) {
uint256 localNumber = 10; // Local variable, exists only in this function
return localNumber;
}
Global variables provide blockchain-related information such as the sender's address, block number, or timestamp.
uint256 public blockNumber = block.number; // Gets the current block number
address public sender = msg.sender; // Gets the address of the sender
The if-else statement allows conditional execution of code based on specific conditions.
function checkEven(uint256 num) public pure returns (string memory) {
if (num % 2 == 0) {
return "Even"; // Returns "Even" if the number is divisible by 2
} else {
return "Odd"; // Returns "Odd" if the number is not divisible by 2
}
}
Mappings store key-value pairs, where keys are unique, and values can be of any type.
mapping(address => uint256) public balances; // Maps addresses to balances
function updateBalance(address _user, uint256 _amount) public { balances[_user] = _amount; // Updates the balance for the user }
Structs are used to define custom data structures, grouping multiple data fields.
struct Student { string name; uint256 age; }Student public student; // Declares a student struct variable
function setStudent(string memory _name, uint256 _age) public { student = Student(_name, _age); // Assigns values to the student struct }
Events in Solidity allow logging data on the blockchain. They are mainly used to track actions like transactions or contract updates.
event UserRegistered(address indexed user, uint256 timestamp); // Declares an event
function registerUser() public { emit UserRegistered(msg.sender, block.timestamp); // Emits an event when a user registers }
Modifiers define rules that must be met before executing a function. They help enforce access control and conditions.
modifier onlyOwner() { require(msg.sender == owner, "Not the owner"); // Checks if the caller is the contract owner _; }
function restrictedFunction() public onlyOwner { // Function logic that only the owner can execute }
Payable functions allow contracts to receive and send Ether. The msg.value property holds the amount of Ether sent.
function deposit() public payable { require(msg.value > 0, "Must send some Ether"); // Ensures Ether is sent }
function getBalance() public view returns (uint256) { return address(this).balance; // Returns the contract's balance }
- Book: Mastering Ethereum (Chapter 7: Smart Contracts and Solidity).
- Practice: Write a simple "Hello World" contract in Remix IDE.
Week 3: Advanced Data Structures
WEEK 3 PRESENTATION
- Arrays: Fixed-size and dynamic arrays.
- Structs: Custom data types.
- Mappings: Key-value pairs.
- Enums: User-defined types for constants.
In this session, we explore Arrays, Structs, Mappings, and Enums in depth, which are essential for smart contract development.
- β Arrays: Storing multiple values.
- β Structs: Grouping multiple pieces of data.
- β Mappings: Storing key-value pairs.
- β Enums: Defining fixed choices.
An array is a list that holds multiple values of the same type.
- πΉ Fixed-size array β has a set number of items.
- πΉ Dynamic array β can grow or shrink.
// A fixed-size array that holds 3 numbers
uint[3] numbers = [10, 20, 30];
uint[] numbers; // Can grow or shrinkfunction addNumber(uint _num) public { numbers.push(_num); // Adds a number to the array }
function removeLast() public { numbers.pop(); // Removes the last number }
function getAllNumbers() public view returns (uint[] memory) {
return numbers;
}
A struct allows you to combine multiple data types into a single entity.
struct Student {
string name;
uint age;
bool enrolled;
}
Student public student;
function setStudent(string memory _name, uint _age, bool _enrolled) public { student = Student(_name, _age, _enrolled); }
Student[] public students;
function addStudent(string memory _name, uint _age, bool _enrolled) public { students.push(Student(_name, _age, _enrolled)); }
A mapping is a key-value store, like a dictionary.
mapping(address => uint) public balances;function deposit(uint _amount) public { balances[msg.sender] += _amount; }
function checkBalance() public view returns (uint) { return balances[msg.sender]; }
β
Key: msg.sender (userβs wallet address)
β
Value: The amount of tokens the user has
You can have mappings inside mappings! For example, each user can have multiple token balances.
mapping(address => mapping(string => uint)) public tokenBalances;
function setTokenBalance(string memory _token, uint _amount) public {
tokenBalances[msg.sender][_token] = _amount;
}
function getTokenBalance(string memory _token) public view returns (uint) {
return tokenBalances[msg.sender][_token];
}
An enum (short for βenumerationβ) is used when you have a fixed set of choices.
enum OrderStatus { Pending, Shipped, Delivered }
OrderStatus public status;
function setStatus(uint _status) public {
status = OrderStatus(_status); // 0 = Pending, 1 = Shipped, 2 = Delivered
}
function isDelivered() public view returns (bool) {
return status == OrderStatus.Delivered;
}
This smart contract simulates an online store using all the data structures.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract OnlineShop {
enum OrderStatus { Pending, Shipped, Delivered }
struct Product {
string name;
uint price;
}
struct Order {
address buyer;
uint productId;
OrderStatus status;
}
Product[] public products;
mapping(uint => Order) public orders;
uint public orderCount;
function addProduct(string memory _name, uint _price) public {
products.push(Product(_name, _price));
}
function placeOrder(uint _productId) public {
require(_productId < products.length, "Invalid product ID");
orders[orderCount] = Order(msg.sender, _productId, OrderStatus.Pending);
orderCount++;
}
function updateOrderStatus(uint _orderId, OrderStatus _status) public {
require(_orderId < orderCount, "Invalid order ID");
orders[_orderId].status = _status;
}
function getOrder(uint _orderId) public view returns (address, string memory, uint, OrderStatus) {
require(_orderId < orderCount, "Invalid order ID");
Order storage order = orders[_orderId];
Product storage product = products[order.productId];
return (order.buyer, product.name, product.price, order.status);
}
}
- β Arrays β Store multiple values in a list.
- β Structs β Group different types of data together.
- β Mappings β Store key-value pairs for quick lookups.
- β Enums β Define fixed choices for specific conditions.
Run the following command in your project directory:
npm install --save-dev hardhatIf you haven't initialized Hardhat, run:
npx hardhatSelect "Create a basic sample project" and follow the prompts.
Ensure your hardhat.config.js or hardhat.config.ts file has the correct Solidity compiler version:
module.exports = {
solidity: "0.8.20",
};Run:
npx hardhat compileThis will compile all Solidity files in the contracts/ directory and store the artifacts in artifacts/ and cache/.
If you encounter errors, review them in the terminal and adjust your Solidity code or compiler version accordingly.
Check the artifacts/contracts/ directory to ensure the .json files (ABI & Bytecode) are generated.
Now your Solidity code is compiled successfully using Hardhat! π
Try adding a feature where users can leave reviews for products.
- Book: Mastering Ethereum (Chapter 7: Smart Contracts and Solidity).
Week 4: Control Structures and Error Handling
- Conditional statements:
if,else,else if. - Loops:
for,while. - Error handling:
require,assert,revert.
Welcome to Week 4 of Solidity Mondays! This session focuses on control flow and defensive programming in Solidity. We'll explore conditional logic, loops, and error handling with examples from our OnlineShop smart contract.
Solidity allows if, else if, and else to control decision-making in your contracts.
function getBuyerTier(uint totalSpent) public pure returns (string memory) {
if (totalSpent >= 1000 ether) {
return "Platinum";
} else if (totalSpent >= 500 ether) {
return "Gold";
} else {
return "Regular";
}
}This helps the contract give loyalty rewards based on how much a user has spent.
Loops are useful for iterating through arrays or performing repeated actions. Use them carefully to avoid high gas consumption!
function totalProductPrice() public view returns (uint total) {
for (uint i = 0; i < products.length; i++) {
total += products[i].price;
}
}function countPendingOrders() public view returns (uint count) {
uint i = 0;
while (i < orderCount) {
if (orders[i].status == OrderStatus.Pending) {
count++;
}
i++;
}
}Solidity offers three main tools for handling errors: require, assert, and revert.
function placeOrder(uint _productId) public {
require(_productId < products.length, "Invalid product ID");
orders[orderCount] = Order(msg.sender, _productId, OrderStatus.Pending);
orderCount++;
}function checkOrderExists(uint _orderId) public view {
assert(_orderId <= orderCount); // Should always be true if order creation works
}function cancelOrder(uint _orderId) public {
if (_orderId >= orderCount) {
revert("Order does not exist");
}
delete orders[_orderId];
}Use these tools to stop transactions when conditions aren't met β saving gas and preventing bugs!
| Keyword | Use Case | Reverts? | Custom Message? |
|---|---|---|---|
require() |
Input validation, permissions | β | β |
assert() |
Invariants, internal logic checks | β | β |
revert() |
Manual error handling | β | β |
pragma solidity ^0.8.0;
contract OnlineShop {
enum OrderStatus { Pending, Shipped, Delivered }
struct Product {
string name;
uint price;
}
struct Order {
address buyer;
uint productId;
OrderStatus status;
}
Product[] public products;
mapping(uint => Order) public orders;
uint public orderCount;
function addProduct(string memory _name, uint _price) public {
products.push(Product(_name, _price));
}
function placeOrder(uint _productId) public {
require(_productId < products.length, "Invalid product ID");
orders[orderCount] = Order(msg.sender, _productId, OrderStatus.Pending);
orderCount++;
}
function updateOrderStatus(uint _orderId, OrderStatus _status) public {
require(_orderId < orderCount, "Invalid order ID");
orders[_orderId].status = _status;
}
function getOrder(uint _orderId) public view returns (address, string memory, uint, OrderStatus) {
require(_orderId < orderCount, "Invalid order ID");
Order storage order = orders[_orderId];
Product storage product = products[order.productId];
return (order.buyer, product.name, product.price, order.status);
}
function totalProductPrice() public view returns (uint total) {
for (uint i = 0; i < products.length; i++) {
total += products[i].price;
}
}
function countPendingOrders() public view returns (uint count) {
uint i = 0;
while (i < orderCount) {
if (orders[i].status == OrderStatus.Pending) {
count++;
}
i++;
}
}
function cancelOrder(uint _orderId) public {
if (_orderId >= orderCount) {
revert
Week 5: Function Modifiers and Events
- Function modifiers:
view, pure, payable.
- Custom modifiers.
- Events: Logging and listening to events.
Welcome to today's Solidity Mondays!
Today, we'll dive deep into function modifiers, custom modifiers, and events β key concepts for writing clean, secure smart contracts.
In Solidity, modifiers tell us how a function interacts with the blockchain.
- A
view function promises NOT to modify the blockchain state.
- It can read state variables but cannot change them.
- Useful for fetching data without paying gas (if called externally).
uint public balance;
function getBalance() public view returns (uint) {
return balance;
}
- A
pure function neither reads nor modifies the blockchain state.
- It only works with its own parameters or internal variables.
- Perfect for mathematical calculations.
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
- A
payable function can receive Ether.
- Without it, sending ETH to a function will fail.
function deposit() public payable {
// Contract can now receive ETH
}
Modifier
Reads State?
Modifies State?
Can Receive Ether?
Purpose
view
β
β
β
Read blockchain state
pure
β
β
β
Pure computation only
payable
β
(optional)
β
(optional)
β
Accept ETH payments
Custom modifiers are reusable rules you can apply to functions to enforce conditions.
- Define a
modifier that runs some checks.
- Use
_; to represent where the original function should continue if checks pass.
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
function withdraw() public onlyOwner {
payable(msg.sender).transfer(address(this).balance);
}
require checks that only the contract owner can call withdraw.
_; tells Solidity where the withdraw function should continue after the check.
modifier costs(uint price) {
require(msg.value >= price, "Not enough Ether");
_;
}
function buy() public payable costs(1 ether) {
// User must send at least 1 ETH
}
Events are like announcements that something important happened inside your contract.
They are recorded on the blockchain logs and are used to communicate with frontends.
- Define an Event:
event Deposited(address indexed user, uint amount);
indexed lets users filter by address when searching.
- Emit the Event inside a function:
function deposit() public payable {
emit Deposited(msg.sender, msg.value);
}
- Listen to Events from the frontend (example with ethers.js):
contract.on("Deposited", (user, amount) => {
console.log(`${user} deposited ${amount} wei!`);
});
- Track activities (e.g., deposits, withdrawals).
- Update frontend UIs in real-time.
- Reduce expensive state writes (cheaper than storage).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleBank {
address public owner;
uint public totalBalance;
event Deposited(address indexed sender, uint amount);
event Withdrawn(address indexed receiver, uint amount);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Caller is not the owner");
_;
}
function deposit() public payable {
totalBalance += msg.value;
emit Deposited(msg.sender, msg.value);
}
function withdraw(uint _amount) public onlyOwner {
require(totalBalance >= _amount, "Insufficient balance");
totalBalance -= _amount;
payable(msg.sender).transfer(_amount);
emit Withdrawn(msg.sender, _amount);
}
function getBalance() public view returns (uint) {
return totalBalance;
}
function calculateBonus(uint amount) public pure returns (uint) {
return amount * 10 / 100;
}
}
view, pure, and payable describe function behaviors.
- Custom modifiers help you enforce rules and conditions efficiently.
- Events are critical for logging actions and communicating with dApps and UIs.
Mastering these areas will level up your ability to write secure, scalable, and professional smart contracts!
Write a smart contract:
- Users can deposit ETH (
payable function).
- Only users who deposited can withdraw (custom modifier).
- Emit events when deposits and withdrawals happen.
Week 6: Inheritance and Interfaces
- Inheritance:
is keyword, parent and child contracts.
- Abstract contracts.
- Interfaces: Defining and implementing interfaces.
Week 6: Inheritance, Abstract Contracts, and Interfaces
- ERC-20: Fungible tokens.
- ERC-721: Non-fungible tokens (NFTs).
- ERC-1155: Multi-token standard.
Welcome back to Solidity Mondays!
In this session, weβre diving into three advanced but powerful topics: Inheritance, Abstract Contracts, and Interfaces β using real-world ERC-20 token contract examples.
Solidity supports inheritance, which allows you to create a base contract and extend it into child contracts. This helps with code reuse, structure, and modularity. ERC-20 tokens use inheritance to add features like minting and burning.
- Use
is to make a contract inherit from another.
- The child contract gains access to all public and internal variables/functions of the parent.
// Base ERC20 contract
contract ERC20 {
mapping(address => uint256) public balanceOf;
uint256 public totalSupply;
string public name;
string public symbol;
constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
}
function transfer(address to, uint256 amount) public virtual returns (bool) {
require(balanceOf[msg.sender] >= amount, "Not enough balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
}
// Child token with minting
contract MyToken is ERC20 {
address public owner;
constructor() ERC20("MyToken", "MTK") {
owner = msg.sender;
}
function mint(address to, uint256 amount) public {
require(msg.sender == owner, "Only owner can mint");
balanceOf[to] += amount;
totalSupply += amount;
}
}
MyToken inherits from ERC20 and adds custom minting logic.
An abstract contract is a contract that cannot be deployed directly.
It contains at least one unimplemented function. ERC-20 token libraries often use abstract contracts to enforce structure.
abstract contract AbstractERC20 {
mapping(address => uint256) public balanceOf;
uint256 public totalSupply;
function transfer(address to, uint256 amount) public virtual returns (bool);
function decimals() public view virtual returns (uint8);
}
contract RealERC20 is AbstractERC20 {
string public name = "RealToken";
string public symbol = "RTL";
constructor() {
balanceOf[msg.sender] = 1000;
totalSupply = 1000;
}
function transfer(address to, uint256 amount) public override returns (bool) {
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
function decimals() public pure override returns (uint8) {
return 18;
}
}
AbstractERC20 defines structure, while RealERC20 provides implementation.
Interfaces are like contracts but can only declare functions β they cannot contain any implementation or state.
They are widely used to define standards like ERC-20 so that wallets, DEXs, and other smart contracts can interact with tokens seamlessly.
- All functions must be
external and unimplemented.
- No constructor, state variables, or function bodies.
// Interface
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);
}
// Implementing Contract
contract MiniToken is IERC20 {
mapping(address => uint256) private _balances;
uint256 private _supply;
constructor() {
_supply = 1000;
_balances[msg.sender] = _supply;
}
function totalSupply() external view override returns (uint256) {
return _supply;
}
function balanceOf(address account) external view override returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) external override returns (bool) {
require(_balances[msg.sender] >= amount, "Not enough tokens");
_balances[msg.sender] -= amount;
_balances[to] += amount;
return true;
}
}
MiniToken conforms to the IERC20 interface for ERC-20 compatibility.
Concept
Description
ERC-20 Example
Inheritance
Child contracts inherit variables and functions from base contracts.
MyToken is ERC20
Abstract Contracts
Define structure but not full logic; must be extended.
AbstractERC20 with transfer()
Interfaces
Only declare external functions β no implementation.
IERC20
π That wraps up Week 6 of Solidity Mondays. You're now equipped with tools to structure smart contracts like ERC-20 tokens β using inheritance, abstract contracts, and interfaces!
- Online Resources: Solidity Documentation.
- Practice: Create a parent contract with shared functionality and a child contract that inherits from it.
Week 7: Security Best Practices
Welcome to this extended session of Solidity Mondays. Today, we dive into one of the most important topics in smart contract development: SECURITY.
Unlike traditional software, smart contracts are immutable once deployed. This means a bug or vulnerability can't be patched without deploying a new contract. If someone exploits a weakness in your code, it could result in permanent loss of funds, broken logic, or even total contract destruction.
This guide explains key vulnerabilities, how to avoid them, and the tools you can use to protect your smart contracts.
1. Reentrancy Attack
What is it? A reentrancy attack happens when a contract sends ETH to an external contract or address before updating its state. If the external contract has fallback logic (e.g., a receive() or fallback() function), it can re-enter the original contract and repeat actions before the state updates β allowing attackers to withdraw more funds than they should.
Simple example:
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Send failed");
balances[msg.sender] -= _amount;
}
Here, the contract sends ETH before updating the balance, making it vulnerable.
Fix: Use the Checks-Effects-Interactions pattern:
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
(bool sent, ) = msg.sender.call{value: _amount}("");
require(sent, "Send failed");
}
Also consider using OpenZeppelin's ReentrancyGuard to prevent nested calls.
2. Integer Overflow and Underflow
What is it? Integer overflow occurs when a number goes beyond its maximum value (e.g., 255 + 1 = 0 for uint8), and underflow is the opposite (e.g., 0 - 1 = 255).
Why itβs dangerous: Attackers can use it to manipulate balances, counters, or timers.
Example: (before Solidity 0.8)
uint8 x = 255;
x += 1; // x becomes 0, wraps around
Fix: Use Solidity 0.8 or above, which has built-in checks for overflows. For earlier versions, use SafeMath from OpenZeppelin.
3. Unprotected selfdestruct()
What is it? If you expose the selfdestruct function without restricting access, anyone can destroy the contract and send its ETH to any address.
Bad:
function destroy() public {
selfdestruct(payable(msg.sender));
}
Fix: Always limit access to critical functions:
modifier onlyOwner {
require(msg.sender == owner, "Not authorized");
_;
}
function destroy() public onlyOwner {
selfdestruct(payable(owner));
}
4. Timestamp Manipulation
What is it? Miners can slightly control block.timestamp within a range of 15 seconds. If your logic depends on it (e.g., randomness or time-based conditions), it can be manipulated.
Example:
if (block.timestamp % 2 == 0) {
winner = msg.sender;
}
Fix: Use trusted randomness sources like Chainlink VRF. Avoid using block.timestamp or blockhash for random decisions.
5. tx.origin Authentication Flaw
What is it? tx.origin returns the original external account that started the transaction. If a user interacts with a malicious contract that then calls your contract, tx.origin still shows the user β allowing attackers to bypass checks.
Bad:
require(tx.origin == owner);
Fix: Use msg.sender, which represents the immediate caller of the function.
6. Front-running
What is it? If your contract allows users to submit transactions that reveal valuable information (like bids, prices, or timing), others can copy and send similar transactions with a higher gas fee to βcut the line.β
Example: DEX trade or NFT minting price set in the open.
Fix:
- Use commit-reveal schemes
- Design logic to be less predictable (randomize when possible)
- Donβt store sensitive information publicly before execution
7. Unchecked External Calls
What is it? Calling unknown or external contracts can introduce security risks β especially if you donβt check the return value or response.
Fix: Always check if the call was successful. Prefer using interfaces and trusted contracts.
8. Missing Access Controls
What is it? Many developers forget to restrict who can call sensitive functions. This means anyone can call mint(), pause(), or even withdraw().
Fix: Always use access control modifiers like onlyOwner, onlyAdmin, or role-based access via AccessControl from OpenZeppelin.
9. Writing Secure Solidity β Best Practices
- β
Use require() to validate all inputs
- β
Use Checks-Effects-Interactions pattern
- β
Prefer msg.sender over
tx.origin
- β
Avoid complex logic inside fallback or receive functions
- β
Avoid loops that can grow indefinitely β theyβre expensive
- β
Keep functions and contracts as small and modular as possible
- β
Use OpenZeppelin libraries and templates for common patterns
10. Security Tools
pip install slither-analyzer
slither contracts/MyContract.sol
Find issues like reentrancy, unused variables, shadowed functions, and more.
pip install mythril
myth analyze contracts/MyContract.sol
Simulates execution paths to find logic flaws and vulnerabilities.
forge test --fuzz
Fuzz testing: Generate random inputs to find bugs.
Built-in static analysis in the Remix IDE. Very beginner-friendly.
11. Smart Contract Security Checklist
- β
Use the latest Solidity compiler
- β
Validate all inputs using
require()
- β
Use
ReentrancyGuard when sending ETH
- β
Prefer
transfer and call with care β always check success
- β
Write unit tests and run fuzz testing
- β
Set appropriate visibility (public/private/internal)
- β
Avoid relying on block data for randomness
- β
Use OpenZeppelin libraries and audit your code
- β
Remove test functions before deploying to mainnet
12. Recommended Resources
π Always remember: Every line of code is a potential threat vector. Test thoroughly. Audit continuously. Write defensively.
Keep building safely! π
- Common vulnerabilities: Reentrancy, integer overflow, and more.
- Security tools: Slither, MythX.
- Writing secure code.
- Book: Mastering Ethereum (Chapter 9: Smart Contract Security).
- Online Resources: Consensys Smart Contract Best Practices.
- Practice: Audit a simple contract for vulnerabilities.
Week 8: Deploying and Interacting with Contracts
- Deploying contracts to testnets (Ropsten, Rinkeby, etc.).
- Interacting with contracts using Web3.js or Ethers.js.
- Gas optimization techniques.
- Book: Mastering Ethereum (Chapter 10: Tokens).
- Tools: Infura, Alchemy, or Hardhat for deployment.
- Practice: Deploy your voting contract to a testnet and interact with it using a simple frontend.
Week 9: Advanced Topics and Final Project
- Upgradeable contracts using proxies.
- Layer 2 solutions: Optimism, Arbitrum.
- Decentralized Autonomous Organizations (DAOs).
- Book: Mastering Ethereum (Chapter 11: Oracles and Chapter 12: Decentralized Applications).
- Final Project: Build and deploy a decentralized application (dApp) that incorporates everything youβve learned.
- Books:
- Mastering Ethereum by Andreas M. Antonopoulos and Gavin Wood.
- Solidity Programming Essentials by Ritesh Modi.
- Online Courses:
By following this Solidity Fridays schedule, youβll gain a solid understanding of Solidity and Ethereum development. Happy coding! π
Don't forget to follow these ETHAccra channels to get regular Updates.
Telegram: https://t.me/+pXympPbcG7U5NWI0
X (Twitter): https://x.com/ETHAccra

