🚀 The fundamentals of Solidity: production-level contracts. A Journey to Building Decentralized Applications with Real-World Scenario.

Magda Jankowska
The Capital
38 min readSep 25, 2024

--

Introduction

📝 Smart contracts have transformed the way we think about applications, allowing for decentralized, trustless, and transparent systems. This guide teaches you the fundamentals of Solidity, paired with real-world contract examples in production environments. Whether it’s building a token for a decentralized finance (DeFi) project or creating a decentralized voting system, this guide gives you hands-on knowledge.

Week 1: Foundations of Solidity and Smart Contracts

🌐 Introduction to Blockchain & Ethereum

  • Overview: Learn the basics of Ethereum, a decentralized platform for smart contracts.
  • Real-World Scenario: In the world of logistics, companies use blockchain to track shipments. Imagine you’re a developer for a logistics company, creating a smart contract to store package tracking data.
  • Production

Tasks: Create a smart contract to store and update package tracking information.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract PackageTracking {

// Define a struct to store package details.
struct Package {
string description; // A short description of the package.
uint256 timestamp; // The last time the package location was updated.
string location; // The current location of the package.
}

// A mapping to store the details of each package using a unique package ID.
mapping(uint256 => Package) public packageDetails;

/**
* @dev Update the location of a specific package.
* @param _packageId The unique ID of the package.
* @param _location The new location of the package.
*/
function updatePackage(uint256 _packageId, string memory _location) public {
// Retrieve the package from storage using its unique ID.
Package storage p = packageDetails[_packageId];

// Update the location of the package.
p.location = _location;

// Set the current timestamp to mark when the package location was last updated.
p.timestamp = block.timestamp;
}
}

Package Struct:

  • Holds the description, timestamp, and current location of a package.

packageDetails Mapping:

  • Associates a unique package ID (uint256) with a Package struct that stores details for each package.

updatePackage Function:

  • Allows updating the package’s location and stores the time of the update.
  • Uses block.timestamp to store the time when the package location was last modified.

This contract can be used in a real-world scenario where a logistics company tracks packages and updates their location at each checkpoint.

🛠️ Solidity Basics

  • Overview: Write your first smart contract, learning about data types and structures.
  • Real-World Scenario: You’re working on an automated royalty system for musicians. When their song is streamed, they are automatically paid based on agreed terms, with no intermediaries.
  • Production

Tasks: Create a contract that simulates a royalty system, allowing users to claim their earnings.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract RoyaltyPayment {

// A mapping to track the royalty balance of each artist (or recipient).
mapping(address => uint256) public royalties;

/**
* @dev Pay royalties to a specified artist.
* @param _artist The address of the artist who will receive the royalties.
* @param _amount The amount of royalties to be paid to the artist.
*/
function payRoyalties(address _artist, uint256 _amount) public {
// Increase the royalty balance of the artist by the specified amount.
royalties[_artist] += _amount;
}

/**
* @dev Withdraw the royalties accumulated for the caller.
* The caller must have some royalties to withdraw.
*/
function withdrawRoyalties() public {
// Retrieve the amount of royalties available for the caller.
uint256 amount = royalties[msg.sender];

// Ensure that there are royalties to withdraw.
require(amount > 0, "No royalties to withdraw");

// Reset the royalties balance for the caller to 0 before transferring.
royalties[msg.sender] = 0;

// Transfer the royalties to the caller.
payable(msg.sender).transfer(amount);
}
}

royalties Mapping:

  • Tracks the amount of royalties owed to each artist (or address). The key is the artist’s address, and the value is the total amount of royalties that have been accumulated for them.

payRoyalties Function:

  • This function is used to increase the royalty balance for a specific artist.
  • It takes two parameters: _artist, which is the address of the artist, and _amount, which is the amount of royalties to be added to their balance.

withdrawRoyalties Function:

  • Allows artists (or recipients) to withdraw the royalties that have been accumulated for them.
  • It retrieves the amount of royalties owed to the caller (msg.sender), resets their balance to zero to prevent reentrancy attacks, and transfers the amount to the caller’s address.

Music streaming services, online art markets, and content producers can use this contract, which provides for the gradual accumulation of royalties that are paid to creators or artists upon withdrawal of earnings.

🔐 Functions and Visibility

  • Overview: Learn how visibility modifiers affect access to smart contract functions and variables.
  • Real-World Scenario: You’re developing a decentralized insurance system where policy details must remain private, but certain users, like insurance agents, can access specific data.
  • Production

Tasks: Implement a contract where only authorized users can view policy details.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract InsurancePolicy {

// Define a struct to store the details of an insurance policy.
struct Policy {
string policyHolder; // Name of the policyholder.
uint256 coverageAmount; // The total amount of coverage provided by the policy.
}

// A private variable to hold the policy information.
Policy private policy;

// A private variable to store the address of the insurer who issued the policy.
address private insurer;

/**
* @dev Constructor that initializes the policy details.
* @param _policyHolder The name of the policyholder.
* @param _coverageAmount The amount of coverage for the policy.
* The constructor sets the insurer to the address that deploys the contract.
*/
constructor(string memory _policyHolder, uint256 _coverageAmount) {
// Set the insurer to the address that deploys the contract.
insurer = msg.sender;

// Initialize the policy with the provided policyholder name and coverage amount.
policy = Policy(_policyHolder, _coverageAmount);
}

/**
* @dev Get the coverage amount of the policy.
* Only the insurer (the contract deployer) can call this function.
* @return The coverage amount of the policy.
*/
function getCoverage() public view returns (uint256) {
// Ensure that only the insurer can access this function.
require(msg.sender == insurer, "Not authorized");

// Return the coverage amount for the insurance policy.
return policy.coverageAmount;
}
}

Policy Struct:

  • This struct defines the details of an insurance policy, including the policyHolder's name and the coverageAmount.

policy Variable:

  • Stores the insurance policy details. It’s declared as private, meaning it can only be accessed within the contract.

insurer Variable:

  • Holds the address of the entity (insurer) that deployed the contract. Only this address is authorized to retrieve the policy’s coverage amount.

Constructor:

  • Initializes the contract with the insurer’s address (set to the address that deploys the contract) and the policy details (policyholder’s name and coverage amount).
  • The constructor ensures that only the deployer (insurer) can later interact with sensitive functions.

getCoverage Function:

  • A function to retrieve the policy’s coverage amount.
  • It includes a check using require to ensure that only the insurer (who deployed the contract) can access this information.

This contract could represent a simple insurance policy where a policyholder is insured for a specific amount. The coverage details are only accessible to the insurer who created the policy, which may be helpful in circumstances where an insurer must oversee or verify private contract terms without disclosing information to the public.

📊 State Variables and Mappings

  • Overview: Store data using mappings and state variables.
  • Real-World Scenario: A decentralized social network requires mapping users to their profiles and posts. Mappings allow you to track user activity in a decentralized manner.
  • Production

Tasks: Create a contract where users can register their profiles.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SocialNetwork {

// Define a struct to store the user profile details.
struct Profile {
string username; // The username of the profile.
string bio; // A short bio describing the user.
}

// A mapping to associate an Ethereum address with a Profile.
// This mapping stores the profile information for each user.
mapping(address => Profile) public profiles;

/**
* @dev Create or update a profile for the sender's address.
* @param _username The username to be assigned to the profile.
* @param _bio A short bio to describe the user.
*/
function createProfile(string memory _username, string memory _bio) public {
// Assign the username and bio to the profile of the sender (msg.sender).
profiles[msg.sender] = Profile(_username, _bio);
}
}

Profile Struct:

  • The Profile struct holds user-specific information, including a username and a short bio describing the user.

profiles Mapping:

  • The profiles mapping connects an Ethereum address (address) to a user profile (Profile). It allows each user to have a profile associated with their address on the blockchain. This mapping is marked as public, meaning anyone can view the profiles.

createProfile Function:

  • This function allows users to create or update their profile.
  • It takes two parameters: _username (the username to be assigned to the profile) and _bio (a short bio for the user).
  • The function stores the profile information (username and bio) in the profiles mapping, using the msg.sender (the address calling the function) as the key.

This contract simulates a basic decentralized social network where each user can create and update their profile by associating their Ethereum address with a username and bio. This could be used in decentralized applications (DApps) where users maintain a decentralized identity, similar to creating a profile on platforms like Twitter, but the information is stored on the blockchain.

🔄 Conditionals and Loops

  • Overview: Implement decision-making logic using if statements and loops.
  • Real-World Scenario: In a voting system, conditionals help determine if a user has already voted and loops tally the votes in real-time.
  • Production

Tasks: Build a voting system where each user can only vote once.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract VotingSystem {

// A mapping to track whether an address has already voted.
// If `true`, the address has voted; if `false`, they haven't.
mapping(address => bool) public hasVoted;

// A mapping to track the number of votes each candidate has received.
// The key is the candidate ID and the value is the vote count for that candidate.
mapping(uint256 => uint256) public votes;

/**
* @dev Allows an address to cast a vote for a specific candidate.
* Each address can only vote once.
* @param _candidateId The ID of the candidate being voted for.
*/
function vote(uint256 _candidateId) public {
// Ensure the caller has not voted before.
require(!hasVoted[msg.sender], "Already voted");

// Increment the vote count for the specified candidate.
votes[_candidateId]++;

// Mark the caller's address as having voted.
hasVoted[msg.sender] = true;
}

/**
* @dev Tally the votes for all candidates.
* Assumes there are 5 candidates (IDs 0-4).
* @return An array containing the vote counts for all candidates.
*/
function tallyVotes() public view returns (uint256[] memory) {
// Create an array of 5 elements to store the vote results.
uint256;

// Loop through the candidates (IDs 0-4) and record their vote count.
for (uint256 i = 0; i < 5; i++) {
results[i] = votes[i];
}

// Return the array containing the vote counts for each candidate.
return results;
}
}

hasVoted Mapping:

  • This mapping keeps track of whether an address has voted or not. Once a user has voted, their address is marked as true, preventing them from voting again.

votes Mapping:

  • This mapping stores the number of votes each candidate has received. The uint256 key represents the candidate ID, and the value represents the number of votes they have accumulated.

vote Function:

  • This function allows users to vote for a candidate. It first checks if the caller (msg.sender) has already voted using require. If they haven’t, it increments the vote count for the specified candidate (_candidateId) and marks the user as having voted.

tallyVotes Function:

  • This function allows anyone to view the vote tally for all candidates.
  • It creates an array of length 5, assuming there are 5 candidates, and loops through the candidate IDs (0 to 4) to populate the array with the vote counts from the votes mapping.
  • The results are returned as an array.

A decentralized voting system where users can vote for candidates based only on their Ethereum addresses could make use of this contract. The voting counts are safely stored on the blockchain and the system makes sure that each address can only cast one vote. It could be used for organizational decision-making where voting is done on-chain for security and transparency, as well as for governance models and political elections.

🧱 Arrays and Structs

  • Overview: Use arrays and structs to store complex data structures.
  • Real-World Scenario: Develop a decentralized crowdfunding platform where backers can fund different projects, and the project’s status is stored in structs.
  • Production

Tasks: Build a crowdfunding DApp that tracks the amount raised for each project.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Crowdfunding {

// Define a struct to represent a crowdfunding project.
struct Project {
string name; // Name of the project.
address payable creator; // Address of the project creator (payable so they can receive funds).
uint256 goal; // The funding goal the project aims to achieve.
uint256 amountRaised; // The amount of funds raised so far.
}

// An array to store all projects created within the contract.
Project[] public projects;

/**
* @dev Create a new project for crowdfunding.
* @param _name The name of the project.
* @param _goal The funding goal for the project.
* This function adds a new project to the `projects` array, initializing its details.
*/
function createProject(string memory _name, uint256 _goal) public {
// Add a new project to the `projects` array, with the creator set to the sender of the transaction.
projects.push(Project(_name, payable(msg.sender), _goal, 0));
}

/**
* @dev Fund a project by sending ETH to it.
* @param _index The index of the project in the `projects` array to fund.
* This function allows users to contribute funds to a project.
* The value sent with the transaction is added to the project's total `amountRaised`.
*/
function fundProject(uint256 _index) public payable {
// Retrieve the project using the given index from the `projects` array.
Project storage p = projects[_index];

// Increase the project's amount raised by the value of the ETH sent with the transaction.
p.amountRaised += msg.value;
}
}

Project Struct:

  • This struct defines a crowdfunding project with four key fields:
  • name: The project name.
  • creator: The address of the project creator, marked as payable so they can receive funds.
  • goal: The target funding amount the project needs.
  • amountRaised: The total amount of funds raised for the project so far.

projects Array:

  • This dynamic array stores all the projects created within the contract. Each project is represented as an instance of the Project struct.

createProject Function:

  • Allows users to create a new project for crowdfunding.
  • The function takes the project’s name (_name) and the funding goal (_goal) as parameters.
  • A new Project instance is created and added to the projects array, with the sender’s address (msg.sender) being set as the project creator.

fundProject Function:

  • Allows users to fund a specific project by sending ETH.
  • The function takes the index (_index) of the project to fund as a parameter.
  • The value of the ETH sent with the transaction (msg.value) is added to the project’s amountRaised.

This contract represents a basic crowdfunding platform where users can create and fund projects. It’s similar to platforms like Kickstarter or GoFundMe, where people can create campaigns to raise money for their ideas. Contributors send ETH to projects, helping them reach their funding goals.

📢 Events and Logging Data

  • Overview: Emit events to create logs that can be tracked on the blockchain.
  • Real-World Scenario: In a decentralized marketplace, event logs track purchases, allowing customers to view transaction histories and resolve disputes.
  • Production

Tasks: Implement a purchase contract that logs product purchases.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Marketplace {

// Define an event to log when a product is purchased.
// This event will record the buyer's address, the ID of the product, and the amount paid.
event Purchase(address indexed buyer, uint256 productId, uint256 amount);

/**
* @dev Allows a user to purchase a product by sending ETH.
* @param _productId The ID of the product being purchased.
* The function emits a Purchase event, which logs the buyer's address, the product ID, and the amount sent.
*/
function buyProduct(uint256 _productId) public payable {
// Emit a Purchase event to log the buyer, the product ID, and the amount of ETH sent.
emit Purchase(msg.sender, _productId, msg.value);
}
}

Purchase Event:

  • The Purchase event is emitted when a user buys a product. It logs:
  • buyer: The address of the user who purchased the product.
  • productId: The ID of the product being bought.
  • amount: The amount of ETH sent for the purchase.
  • The indexed keyword allows filtering of the buyer address in event logs for better traceability.

buyProduct Function:

  • This function allows users to buy a product by specifying its product ID (_productId) and sending ETH with the transaction (msg.value).
  • When the function is called, it emits a Purchase event to log the purchase details.

This contract represents a decentralized marketplace where users can buy products by sending ETH. The Purchase event ensures transparency and allows buyers, sellers, and external services to track purchases on-chain. This setup can be compared to e-commerce platforms, where transactions are logged for record-keeping, customer service, and auditing purposes.

Week 2: Intermediary Concepts and Smart Contract Security

🔗 Inheritance and Contract Composition

  • Overview: Use inheritance to share common logic across contracts.
  • Real-World Scenario: Build a modular lending platform with different types of loans (home, car, etc.), each inheriting common loan logic.
  • Production

Tasks: Create a base loan contract and implement specific loan types that inherit from it.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Base Loan contract
contract Loan {
// Public state variable to store the interest rate for the loan.
uint256 public interestRate;

/**
* @dev Constructor to initialize the loan contract with a specific interest rate.
* @param _interestRate The percentage interest rate (e.g., 5 for 5%).
*/
constructor(uint256 _interestRate) {
interestRate = _interestRate;
}

/**
* @dev Calculate the interest amount on a given principal.
* @param _amount The principal amount for which interest needs to be calculated.
* @return The interest based on the principal and interest rate.
*/
function calculateInterest(uint256 _amount) public view returns (uint256) {
// Return the calculated interest as (_amount * interestRate) / 100.
return (_amount * interestRate) / 100;
}
}

// HomeLoan contract inherits from the Loan contract.
contract HomeLoan is Loan {
/**
* @dev Constructor to initialize the HomeLoan contract.
* It calls the Loan contract constructor with a fixed interest rate of 5%.
*/
constructor() Loan(5) {}
}

// CarLoan contract inherits from the Loan contract.
contract CarLoan is Loan {
/**
* @dev Constructor to initialize the CarLoan contract.
* It calls the Loan contract constructor with a fixed interest rate of 8%.
*/
constructor() Loan(8) {}
}

Loan Contract:

  • The Loan contract acts as a base contract for different types of loans. It holds the interestRate and provides a method for calculating interest on a given loan amount.
  • interestRate: This public state variable stores the percentage interest rate for the loan (e.g., 5 for 5%).
  • constructor: The constructor initializes the contract with a specific interest rate (_interestRate), passed in when the contract is deployed.
  • calculateInterest Function: This function calculates the interest on a given principal amount (_amount). It uses the formula (principal * interestRate) / 100 to return the interest.

HomeLoan Contract:

  • The HomeLoan contract inherits from the Loan contract.
  • When this contract is deployed, it automatically sets the interest rate to 5% by calling the parent Loan constructor with a value of 5.

CarLoan Contract:

  • The CarLoan contract also inherits from the Loan contract.
  • When this contract is deployed, it sets the interest rate to 8% by calling the parent Loan constructor with a value of 8.

This setup models a loan system where different types of loans (e.g., home loans and car loans) can be created with different interest rates. The Loan contract serves as the base, providing the structure for calculating interest. Subclasses like HomeLoan and CarLoan define specific loan products with preset interest rates. This could be used in decentralized finance (DeFi) platforms where users can take out different types of loans with varying rates.

🔐 Modifiers and Access Control

  • Overview: Control access to functions using custom modifiers.
  • Real-World Scenario: A decentralized access control system ensures that only authorized users (e.g., admins) can perform key functions like pausing the contract.
  • Production

Tasks: Create an admin-controlled contract where only admins can update the state.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract AdminControl {

// Public state variable to store the address of the contract's admin.
address public admin;

// Modifier to restrict access to only the admin.
// This will check if the caller of the function is the admin.
modifier onlyAdmin() {
require(msg.sender == admin, "Not authorized"); // Ensure that only the admin can call the function.
_; // Continue with the execution of the function if the require statement passes.
}

/**
* @dev Function to set a new admin.
* @param _admin The address of the new admin.
* This function can only be called by the current admin due to the `onlyAdmin` modifier.
*/
function setAdmin(address _admin) public onlyAdmin {
admin = _admin; // Update the `admin` state variable with the new admin address.
}
}

admin Variable:

  • This is a public state variable that stores the address of the admin. The admin has special privileges, such as the ability to call certain restricted functions.

onlyAdmin Modifier:

  • This is a custom modifier that restricts access to certain functions. It checks that the caller (msg.sender) is the admin.
  • If the caller is not the admin, the transaction is reverted with the message “Not authorized.”
  • If the condition is satisfied, the function continues to execute (_), otherwise it stops.

setAdmin Function:

  • This function allows the admin to set a new admin address.
  • It uses the onlyAdmin modifier to ensure that only the current admin can call this function.
  • Once the function is called, the admin state variable is updated with the new admin address (_admin).

This contract represents a basic access control system where administrative tasks are restricted to a specific account. A similar system could be used in decentralized governance or role-based access control in an organization, where only authorized administrators can make certain changes, such as appointing new administrators or controlling sensitive functions.

💸 Payable Functions and Ether Transfers

  • Overview: Handle Ether transfers in your contracts with payable functions.
  • Real-World Scenario: Build a decentralized savings account where users deposit and withdraw Ether.
  • Production

Tasks: Implement a deposit and withdrawal system with Ether transfers.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SavingsAccount {
// Mapping to track the balance of each address (user).
mapping(address => uint256) public balances;

/**
* @dev Function to deposit funds into the savings account.
* Users can send Ether to this function to increase their balance.
*/
function deposit() public payable {
// Increase the balance of the sender (msg.sender) by the amount sent (msg.value).
balances[msg.sender] += msg.value;
}

/**
* @dev Function to withdraw a specified amount from the savings account.
* @param _amount The amount of Ether the user wishes to withdraw.
* This function checks if the user has enough balance before allowing the withdrawal.
*/
function withdraw(uint256 _amount) public {
// Require that the sender has enough balance to withdraw the specified amount.
require(balances[msg.sender] >= _amount, "Insufficient funds");

// Deduct the specified amount from the user's balance.
balances[msg.sender] -= _amount;

// Transfer the specified amount to the user's address.
payable(msg.sender).transfer(_amount);
}
}

balances Mapping:

  • This mapping stores the balance for each user’s address. The key is the user’s address, and the value is the corresponding balance in wei (the smallest unit of Ether).

deposit Function:

  • This function allows users to deposit Ether into their savings account.
  • The payable keyword enables the function to receive Ether. When a user calls this function and sends Ether, the deposited amount (msg.value) is added to the user’s balance in the balances mapping.

withdraw Function:

  • This function allows users to withdraw a specified amount of Ether from their savings account.
  • It takes a parameter _amount, which is the amount the user wants to withdraw.
  • A require statement checks if the user has sufficient funds (balances[msg.sender] >= _amount). If not, the transaction is reverted with the message "Insufficient funds."
  • If the user has enough balance, the specified amount is deducted from their balance.
  • Finally, the specified amount is transferred to the user’s address using payable(msg.sender).transfer(_amount).

This contract represents a simple savings account system, allowing users to deposit and withdraw Ether securely. Users can store their funds in a decentralized manner without relying on traditional banks. This could be integrated into a larger decentralized finance (DeFi) application, where users can earn interest on their savings or use their savings as collateral for loans.

⚠️ Error Handling and Reverts

  • Overview: Use error handling techniques like require, assert, and revert.
  • Real-World Scenario: Imagine a lottery system where invalid entries are reverted to prevent cheating or mistakes.
  • Production

Tasks: Build a lottery system that reverts invalid entries.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Lottery {
// Array to store the addresses of players participating in the lottery.
address[] public players;

/**
* @dev Function to enter the lottery.
* Users must send an entry fee of 0.1 ETH to participate.
*/
function enter() public payable {
// Require that the sent value is exactly 0.1 ETH.
require(msg.value == 0.1 ether, "Entry fee is 0.1 ETH");

// Add the sender's address to the players array.
players.push(msg.sender);
}

/**
* @dev Function to pick a random winner from the players.
* The winner receives the entire balance of the lottery contract.
*/
function pickWinner() public {
// Require that at least one player has entered the lottery.
require(players.length > 0, "No players entered");

// Generate a random index based on the number of players.
uint256 index = random() % players.length;

// Transfer the entire balance of the contract to the winner.
payable(players[index]).transfer(address(this).balance);

// Optionally, you could reset the players array here to start a new lottery.
delete players; // Uncomment this line if you want to reset the lottery after a winner is picked.
}

/**
* @dev Private function to generate a pseudo-random number.
* @return A random number generated based on the current block timestamp and players' addresses.
*/
function random() private view returns (uint256) {
// Generate a random number using keccak256 hash function.
// It combines the current block timestamp and the players' addresses to create a unique hash.
return uint256(keccak256(abi.encodePacked(block.timestamp, players)));
}
}

players Array:

  • This array stores the addresses of players who enter the lottery. Each player is identified by their Ethereum address.

enter Function:

  • This function allows users to enter the lottery by sending an entry fee of 0.1 ETH.
  • The require statement checks that the amount sent (msg.value) is exactly 0.1 ETH. If not, the transaction is reverted with an error message.
  • If the requirement is satisfied, the player’s address (msg.sender) is added to the players array.

pickWinner Function:

  • This function randomly selects a winner from the players.
  • It first checks if there are any players in the lottery with require(players.length > 0, "No players entered"). If not, it reverts the transaction.
  • It then generates a random index using the random function, which is constrained by the number of players (% players.length).
  • The winner is determined by the generated index, and the entire balance of the contract is transferred to the winner using payable(players[index]).transfer(address(this).balance).
  • Optionally, you can reset the players’ array by uncommenting delete players; to start a new lottery after a winner has been picked.

random Function:

  • This private function generates a pseudo-random number using the keccak256 hash function.
  • It combines the current block timestamp and the players’ addresses to create a unique hash, which is then converted to a uint256 value. This randomness is not truly secure and should be used with caution in production scenarios due to the potential for manipulation by miners.

This contract models a simple lottery system where participants pay an entry fee to enter a draw. The winner is randomly selected and receives the entire balance of the contract. This can be expanded for use in decentralized applications (dApps), enabling community-driven lotteries or fundraisers. However, for production use, it’s important to implement a more secure method of randomness to prevent manipulation, such as Chainlink VRF (Verifiable Random Function).

🧑‍🤝‍🧑 Multisignature Wallets

  • Overview: Implement multisignature wallets for collaborative decision-making.
  • Real-World Scenario: In company fund management, multiple signatures may be required before funds can be transferred to ensure transparency.
  • Production

Tasks: Create a wallet contract that requires multiple approvals before executing a transaction.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MultisigWallet {
// Array to store the addresses of the owners of the wallet.
address[] public owners;

// Number of required signatures to approve a transaction.
uint256 public requiredSignatures;

// Mapping to track whether a transaction has been signed by an owner.
mapping(uint256 => bool) public isSigned;

/**
* @dev Constructor to initialize the wallet with owners and required signatures.
* @param _owners Array of addresses that will be the owners of the wallet.
* @param _requiredSignatures Number of signatures required to approve a transaction.
*/
constructor(address[] memory _owners, uint256 _requiredSignatures) {
owners = _owners; // Set the owners of the wallet.
requiredSignatures = _requiredSignatures; // Set the required number of signatures.
}

/**
* @dev Function to submit a transaction for signing.
* @param _txId The ID of the transaction to be submitted.
* This function allows wallet owners to sign a transaction.
*/
function submitTransaction(uint256 _txId) public {
// Ensure that the caller is one of the owners of the wallet.
require(isOwner(msg.sender), "Not an owner");

// Mark the transaction as signed by the caller.
isSigned[_txId] = true;
}

/**
* @dev Function to check if an address is one of the owners.
* @param _address The address to check.
* @return bool Returns true if the address is an owner, false otherwise.
*/
function isOwner(address _address) public view returns (bool) {
// Loop through the owners array to check if the provided address is an owner.
for (uint256 i = 0; i < owners.length; i++) {
if (owners[i] == _address) {
return true; // Address is an owner.
}
}
return false; // Address is not an owner.
}
}

owners Array

  • This array stores the addresses of the wallet owners. Each owner has the authority to approve transactions.

requiredSignatures Variable:

  • This variable specifies the number of signatures required to approve a transaction. This helps in ensuring that multiple owners must agree before a transaction is executed.

isSigned Mapping:

  • This mapping tracks whether a transaction (identified by its ID) has been signed by an owner. The key is the transaction ID, and the value is a boolean indicating whether it has been signed.

Constructor:

  • The constructor initializes the contract with the list of owners and the required number of signatures for a transaction to be approved.

submitTransaction Function:

  • This function allows wallet owners to submit a transaction for signing by providing a transaction ID.
  • It uses a require statement to check if the caller is one of the owners using the isOwner function. If not, it reverts with the message "Not an owner."
  • If the caller is an owner, it marks the transaction as signed by setting isSigned[_txId] to true.

isOwner Function:

  • This function checks if a given address is one of the owners of the wallet.
  • It loops through the owners array and returns true if it finds a match. If no match is found, it returns false.

This contract implements a multisignature wallet, which enhances security by requiring multiple approvals for transactions. This setup is common in organizations or communities where financial decisions require consensus among several members, such as in DAOs (Decentralized Autonomous Organizations), joint ventures, or family trusts. By using this contract, organizations can reduce the risk of unauthorized transactions and enhance accountability among members. For a fully functional multisig wallet, additional features like transaction execution and tracking of signatures would be necessary.

🔒 Smart Contract Security Best Practices

  • Overview: Learn about common security risks and mitigation techniques, like reentrancy attacks.
  • Real-World Scenario: In a decentralized exchange (DEX), you’ll prevent reentrancy attacks that could lead to users draining funds from the contract.
  • Production

Tasks: Implement a secure withdrawal function to prevent reentrancy attacks.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SecureDEX {
// Mapping to store the balances of each address.
mapping(address => uint256) public balances;

/**
* @dev Function to deposit Ether into the contract.
* The deposited amount is added to the sender's balance.
*/
function deposit() public payable {
// Increment the sender's balance by the amount sent with the transaction.
balances[msg.sender] += msg.value;
}

/**
* @dev Function to withdraw a specified amount of Ether from the contract.
* @param _amount The amount of Ether to withdraw.
*/
function withdraw(uint256 _amount) public {
// Ensure that the sender has sufficient funds to withdraw the requested amount.
require(balances[msg.sender] >= _amount, "Insufficient funds");

// Store the balance before the withdrawal for later verification.
uint256 beforeBalance = balances[msg.sender];

// Decrement the sender's balance by the withdrawal amount.
balances[msg.sender] -= _amount;

// Transfer the specified amount of Ether to the sender.
payable(msg.sender).transfer(_amount);

// Verify that the balance has been updated correctly using assert.
assert(balances[msg.sender] == beforeBalance - _amount);
}
}

balances Mapping:

  • This mapping tracks the Ether balance of each address. The key is the user’s address, and the value is their balance.

deposit Function:

  • This function allows users to deposit Ether into the contract.
  • The payable modifier enables the function to accept Ether sent along with the transaction.
  • When a user calls this function, the amount they send (msg.value) is added to their balance in the balances mapping.

withdraw Function:

  • This function allows users to withdraw a specified amount of Ether from the contract.
  • It takes a parameter _amount, which is the amount the user wishes to withdraw.

Require Statement:

  • The require statement checks if the user's balance is sufficient to cover the withdrawal. If the balance is less than _amount, the transaction reverts with the message "Insufficient funds."

Balance Management:

  • Before modifying the balance, the current balance is stored in beforeBalance for verification after the withdrawal.

Balance Update:

  • The user’s balance is decremented by the withdrawal amount.

Ether Transfer:

  • The specified amount is transferred to the user’s address using payable(msg.sender).transfer(_amount).

Assertion:

  • Finally, the assert statement checks that the balance has been updated correctly. If the condition fails, the transaction will revert, indicating an inconsistency in the state.

This contract represents a simple decentralized exchange (DEX) where users can deposit and withdraw Ether. This kind of contract could be part of a larger DEX ecosystem that allows users to trade tokens or assets securely. However, this contract lacks additional features such as handling token swaps, security measures against reentrancy attacks (e.g., using the checks-effects-interactions pattern or ReentrancyGuard), and user management. For a production-ready DEX, it’s essential to incorporate robust security practices and additional functionalities.

Week 3: Advanced Concepts and Real-World DApps

🛠️ Integrating Oracles

  • Overview: Use oracles to fetch off-chain data in your smart contracts.
  • Real-World Scenario: Build a decentralized prediction market where users can bet on future events based on off-chain data like election results.
  • Production

Tasks: Integrate Chainlink oracles to fetch real-world data.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Importing the AggregatorV3Interface from the Chainlink library.
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract PredictionMarket {
// Instance of the AggregatorV3Interface to interact with the price feed.
AggregatorV3Interface public priceFeed;

/**
* @dev Constructor to initialize the price feed interface.
* @param _priceFeed The address of the Chainlink price feed contract.
*/
constructor(address _priceFeed) {
// Set the priceFeed to the provided address.
priceFeed = AggregatorV3Interface(_priceFeed);
}

/**
* @dev Function to retrieve the latest price from the price feed.
* @return int256 The latest price as an integer.
*/
function getPrice() public view returns (int256) {
// Fetch the latest price data from the price feed.
// The latestRoundData function returns multiple values, but only the price is used here.
(, int256 price, , , ) = priceFeed.latestRoundData();

// Return the latest price.
return price;
}
}

Importing AggregatorV3Interface:

  • The contract imports the AggregatorV3Interface from the Chainlink library, which provides a standard interface for fetching price data from Chainlink price feeds.

priceFeed Variable:

  • This variable holds an instance of the AggregatorV3Interface, allowing the contract to interact with the specified price feed.

Constructor:

  • The constructor takes an address _priceFeed, which is the address of the deployed Chainlink price feed contract.
  • It initializes the priceFeed variable with this address, allowing the contract to retrieve price data from that specific price feed.

getPrice Function:

  • This public function retrieves the latest price from the price feed.
  • The function calls latestRoundData() on the priceFeed instance, which returns several values related to the price feed, including the latest price. In this case, only the price (int256 price) is captured; the other returned values are ignored (represented by the commas).

Return Statement:

  • The function returns the latest price as an int256 value, which can be used in the prediction market logic.

This contract can serve as part of a prediction market platform, where users can bet on the outcome of events based on current market prices (e.g., sports outcomes, financial markets). By integrating Chainlink’s price feed, the contract can ensure that it operates based on reliable and tamper-proof data. In a complete implementation, additional functions would likely be needed for placing bets, resolving outcomes, and distributing rewards based on the prediction results.

🧑‍💻 Contract Upgradability

  • Overview: Implement upgradeable contracts using proxy patterns.
  • Real-World Scenario: In a stablecoin system, being able to upgrade contracts ensures that your token remains secure and adaptable as regulations change.
  • Production

Tasks: Create a proxy contract that allows upgrading the logic behind the scenes.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Proxy {
// Address of the current implementation contract.
address public implementation;

/**
* @dev Function to upgrade the implementation contract.
* @param _newImplementation The address of the new implementation contract.
*/
function upgrade(address _newImplementation) public {
// Update the implementation address to the new implementation.
implementation = _newImplementation;
}

/**
* @dev Fallback function that delegates all calls to the implementation contract.
* This function is executed when the contract is called without matching function signatures.
*/
fallback() external payable {
// Delegate the call to the implementation contract using the current msg.data.
// This allows the proxy to forward calls to the implementation.
(bool success, ) = implementation.delegatecall(msg.data);

// Require that the delegatecall was successful; if not, revert the transaction.
require(success, "Delegatecall failed");
}
}

implementation Variable:

  • This variable holds the address of the current implementation contract. The proxy will forward calls to this implementation.

upgrade Function:

  • This public function allows the owner or authorized entity to upgrade the implementation contract to a new address.
  • It takes a parameter _newImplementation, which is the address of the new implementation contract.
  • The function updates the implementation variable to point to the new contract, enabling the proxy to utilize the new logic.

fallback Function:

  • The fallback function is triggered whenever the contract receives a call that does not match any function signature.
  • This function is marked as external and payable, meaning it can receive Ether.

Delegatecall:

  • Inside the fallback function, the delegatecall function is used to forward the call to the current implementation contract.
  • The call is forwarded with msg.data, which contains the calldata of the original transaction.
  • Using delegatecall allows the proxy to execute the logic of the implementation contract while maintaining the context (i.e., storage and address) of the proxy.

Success Check:

  • After the delegatecall, the success status is checked. If the call fails, the transaction is reverted with the message "Delegatecall failed."

This Proxy contract is commonly used in upgradeable contracts where it is essential to modify the contract logic without losing state or data. In a decentralized application (DApp), developers might want to deploy new features or fix bugs without requiring users to migrate to a new contract. By using this proxy pattern, the application can maintain its state while changing its logic through upgrades, which is crucial for applications that expect to evolve over time, such as DeFi protocols or NFT marketplaces.

⚡ Gas Optimization Techniques

  • Overview: Learn how to reduce gas costs in your contracts by optimizing storage and computation.
  • Real-World Scenario: In a DeFi lending platform, high gas costs can make loans uneconomical. Optimization helps keep costs low for users.
  • Production

Tasks: Implement gas-saving techniques in your DApp.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract OptimizedStorage {
// State variable 'data' to store a single uint256 value.
uint256 public data;

/**
* @dev Function to update the stored data using inline assembly for optimization.
* @param _newData The new value to be stored in the 'data' variable.
*/
function updateData(uint256 _newData) public {
// Inline assembly is used here to directly access and modify storage.
assembly {
// Use 'sstore' to store the '_newData' at the storage slot where 'data' is located.
// 'data.slot' gives the storage slot of the 'data' variable.
sstore(data.slot, _newData)
}
}
}

data Variable:

  • This public variable data is of type uint256 and holds a single unsigned integer. Since it's public, a getter function is automatically created, allowing external users to view the value of data.

updateData Function:

  • This function takes an input _newData of type uint256, which is intended to replace the current value stored in data.

Assembly Code:

  • Inline assembly is used to directly access the storage slot where the data variable is stored. This can sometimes offer performance optimizations.

sstore:

  • sstore is an EVM opcode used to write to a specific storage location.
  • data.slot gives the storage slot index of the data variable.
  • The sstore(data.slot, _newData) command stores the _newData value in the storage slot where data is located.
  • By using inline assembly, developers can bypass Solidity’s higher-level abstractions, which sometimes adds overhead. This can be useful in performance-critical applications, although it should be used carefully since it may make the code less readable and harder to maintain.

This contract might be used in gas-optimized applications where frequent state updates happen, such as in DeFi protocols or games where storage reads/writes occur in large numbers. Directly managing storage through assembly allows developers to optimize gas usage, but it’s a tradeoff between readability and performance. This approach could be part of a larger system, like a high-frequency trading bot, where every bit of optimization counts.

🛠️ NFT Minting and Marketplace

  • Overview: Create and mint NFTs, then build a marketplace for trading them.
  • Real-World Scenario: You’re building a digital art marketplace where creators can mint and sell their unique works as NFTs.
  • Production

Tasks: Mint your first NFT and list it on a decentralized marketplace.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Importing the ERC721 contract from OpenZeppelin, which provides standard NFT functionality.
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract ArtNFT is ERC721 {
// Counter to keep track of the token IDs for minting new tokens.
uint256 public tokenCounter;

/**
* @dev Constructor that initializes the ERC721 token with a name and symbol.
* It also sets the initial value of the tokenCounter to 0.
*/
constructor() ERC721("ArtToken", "ART") {
// Initialize the token counter to 0.
tokenCounter = 0;
}

/**
* @dev Function to mint a new NFT (Non-Fungible Token).
* It assigns the current value of tokenCounter as the token ID to the sender's address.
*/
function mintToken() public {
// Mint a new NFT with token ID equal to the current value of tokenCounter.
// The msg.sender is the address that will own the newly minted token.
_mint(msg.sender, tokenCounter);

// Increment the tokenCounter to ensure each NFT has a unique token ID.
tokenCounter++;
}
}

ERC721 Inheritance:

  • This contract extends the ERC721 standard from OpenZeppelin, which provides all the necessary functionality for creating NFTs. The ERC721 implementation includes standard methods like transferFrom and safeTransferFrom, making the contract compliant with the ERC721 standard.

tokenCounter Variable:

  • tokenCounter is a uint256 variable that keeps track of the number of NFTs minted and is used to generate unique token IDs for each new token.
  • Since it’s public, a getter function is automatically created, allowing users to query the current token count.

Constructor:

  • The constructor calls the parent ERC721 constructor, passing the name "ArtToken" and the symbol "ART" for the token.
  • These parameters define the name and ticker symbol of the NFT collection (e.g., “ArtToken” would be the collection name, and “ART” the symbol used for it).

mintToken Function:

  • This function allows users to mint (create) new NFTs. It mints a new token for the caller (msg.sender) and assigns the current tokenCounter as the token ID.
  • The _mint function is a part of the OpenZeppelin ERC721 implementation, which handles the token creation process and ensures the token follows the ERC721 standard.
  • After minting, the tokenCounter is incremented to ensure each subsequent token has a unique ID.

This ArtNFT contract can be used by digital artists or NFT platforms to mint unique pieces of art as NFTs on the blockchain. Each NFT will have a unique token ID, and users can own, trade, or display these tokens in their wallets. This contract is suitable for platforms like OpenSea or Rarible, where artists create and sell their digital art pieces, and the ownership is tracked immutably on the blockchain.

💱 Decentralized Exchanges (DEX)

  • Overview: Build a basic decentralized exchange for token swaps.
  • Real-World Scenario: Imagine launching your own DEX, allowing users to swap between different tokens directly on-chain.
  • Production

Tasks: Build a token swap feature for a simple DEX.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Importing the IERC20 interface from OpenZeppelin, which provides the standard ERC20 functions.
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract TokenSwap {
// Two ERC20 tokens involved in the token swap.
IERC20 public token1;
IERC20 public token2;

/**
* @dev Constructor to initialize the two tokens for swapping.
* @param _token1 The address of the first token's contract.
* @param _token2 The address of the second token's contract.
*/
constructor(address _token1, address _token2) {
// Assign the token contract addresses to the state variables.
token1 = IERC20(_token1);
token2 = IERC20(_token2);
}

/**
* @dev Function to swap an amount of token1 for an equal amount of token2.
* @param _amount The amount of token1 to be swapped for token2.
*/
function swap(uint256 _amount) public {
// Ensure that the user has allowed the contract to spend at least the specified amount of token1.
require(token1.allowance(msg.sender, address(this)) >= _amount, "Token 1 allowance too low");

// Ensure that the contract has enough token2 to complete the swap.
require(token2.balanceOf(address(this)) >= _amount, "Token 2 balance too low");

// Transfer token1 from the user to the contract.
token1.transferFrom(msg.sender, address(this), _amount);

// Transfer token2 from the contract to the user.
token2.transfer(msg.sender, _amount);
}
}

IERC20 Interface:

  • The contract imports the IERC20 interface from OpenZeppelin, which provides the standard functions for interacting with ERC20 tokens. This allows the contract to call functions like transfer, transferFrom, allowance, and balanceOf on the token contracts.

State Variables:

  • token1 and token2 are two instances of IERC20, representing the two ERC20 tokens involved in the swap. These variables store the addresses of the token contracts.

Constructor:

  • The constructor accepts two parameters: _token1 and _token2, which are the addresses of the ERC20 token contracts. These are stored in the state variables token1 and token2.

swap Function:

  • This function allows users to swap _amount of token1 for an equal _amount of token2.

Allowance Check:

  • The require statement checks if the user has allowed the contract to spend at least _amount of token1 on their behalf using the allowance function. If the allowance is less than _amount, the transaction reverts with an error message.

Balance Check:

  • The contract ensures it has enough token2 in its balance to complete the swap.

Transfer token1:

  • The contract uses transferFrom to move _amount of token1 from the user's wallet to the contract's address. This function requires the user to first approve the contract to spend their tokens.

Transfer token2:

  • After receiving token1, the contract transfers an equal _amount of token2 to the user.

This contract can be used in a decentralized exchange (DEX) for swapping one ERC20 token for another without involving a third-party intermediary. For example, a DEX platform like Uniswap or SushiSwap could implement similar logic, allowing users to swap tokens directly with each other. In production, this contract would require liquidity (a pool of both tokens) to function effectively. Token swaps like this are critical for decentralized finance (DeFi) ecosystems, enabling users to trade assets in a decentralized manner.

🌍 Cross-Chain Interoperability

  • Overview: Enable communication between different blockchains using cross-chain technology.
  • Real-World Scenario: You want to launch a cross-chain bridge for transferring tokens between Ethereum and Binance Smart Chain.
  • Production

Tasks: Implement a cross-chain bridge that allows token transfers between blockchains.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CrossChainBridge {
// Event that will be emitted whenever tokens are bridged to another chain.
// `from` is the address of the user who is bridging tokens.
// `amount` is the amount of tokens being bridged.
// `destinationChain` is the name of the chain to which the tokens are being sent.
event TokenBridged(address indexed from, uint256 amount, string destinationChain);

/**
* @dev Function to bridge tokens to another blockchain.
* @param _amount The number of tokens to bridge.
* @param _destinationChain The name of the blockchain to which tokens are being sent.
*/
function bridgeTokens(uint256 _amount, string memory _destinationChain) public {
// Emit an event to signal that tokens are being bridged to another chain.
// The event contains the sender's address, the amount of tokens, and the destination chain.
emit TokenBridged(msg.sender, _amount, _destinationChain);
}
}

Event Declaration:

  • The contract defines an event TokenBridged that logs the bridging action. It includes:
  • from: The address of the user sending the tokens.amount: The number of tokens being bridged.
  • destinationChain: A string representing the name of the chain where the tokens are being sent.

indexed keyword: The from parameter is marked as indexed, which allows it to be searchable in the logs, making it easier to filter for specific users when analyzing blockchain events.

bridgeTokens Function:

This function allows a user to bridge tokens to another blockchain by specifying:

  • _amount: The number of tokens they want to bridge.
  • _destinationChain: The name of the blockchain where they want to send the tokens (e.g., Ethereum to Binance Smart Chain).
  • Inside the function, an event TokenBridged is emitted, logging the action with the sender's address, the amount of tokens, and the target blockchain.

In cross-chain token transfers, tokens on one blockchain are locked in a smart contract, and equivalent tokens are minted or released on another blockchain. This contract can serve as a simplified representation of a cross-chain bridge. In a real-world scenario, platforms like AnySwap or Polygon Bridge use similar contracts to facilitate movement of tokens between chains like Ethereum and Binance Smart Chain.

For example, if a user wants to move tokens from Ethereum to Polygon, they would deposit tokens on the Ethereum side, which would emit an event (like TokenBridged), and the tokens would be minted on the Polygon side. This contract provides a basic framework for logging such bridging events.

🛡️ Decentralized Identity (DID)

  • Overview: Implement decentralized identity solutions for secure and privacy-preserving authentication.
  • Real-World Scenario: Build a decentralized identity verification system for a hiring platform, where users control their credentials.
  • Production

Tasks: Create a decentralized identity system for user authentication.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract DecentralizedID {
// Define a struct to store the identity information of a user.
struct Identity {
string name; // The name of the user
string email; // The email address of the user
string hashedData; // Hashed data (could be additional info like address, SSN, etc.)
}

// A mapping that links each Ethereum address to its Identity struct.
mapping(address => Identity) public identities;

/**
* @dev Function to create a decentralized identity for the user.
* @param _name The name of the user.
* @param _email The email address of the user.
* @param _hashedData Additional hashed data (this could represent encrypted personal info or metadata).
*/
function createID(string memory _name, string memory _email, string memory _hashedData) public {
// The msg.sender's address will be mapped to a new Identity struct containing their name, email, and hashed data.
identities[msg.sender] = Identity(_name, _email, _hashedData);
}
}

Struct Definition:

  • The Identity struct is used to store basic identity information:
  • name: Stores the user's name.
  • email: Stores the user's email.
  • hashedData: Stores additional hashed data that could include other personal identifiers or encrypted metadata, allowing flexibility for more complex identity verification scenarios.

Mapping:

  • identities is a mapping that associates an Ethereum address (msg.sender) with an Identity struct, meaning each address can have its own dece
  • decentralized identity.

createID Function:

  • This function allows users to register or create their decentralized identity.
  • It takes in three parameters:
  • _name: The user's name.
  • _email: The user's email address.
  • _hashedData: A hashed version of sensitive data such as SSN or other private identifiers.
  • The function stores this data in the identities mapping, using the sender's Ethereum address as the key.

This contract could be used as part of a decentralized identity management system. Such systems are key components of blockchain-based identity verification platforms, such as Civic or SelfKey. Users can control their personal information on-chain, ensuring privacy and security while also enabling selective sharing with trusted services.

For example, if a user wants to sign up for a service using their decentralized identity, they can provide just the required details (like their name or email) to the service while keeping other details (stored in hashedData) private or encrypted. This enables self-sovereign identity where users fully control their own data without relying on centralized identity providers.

🌉 Deploying to Mainnet

  • Overview: Deploy your smart contracts to the Ethereum mainnet using Hardhat, and verify the contract on Etherscan.
  • Real-World Scenario: You’ve completed your decentralized application, and it’s time to deploy the DApp to the Ethereum network for real users.
  • Tasks: Use Hardhat or Foundry to deploy your contract and verify it on Etherscan.

Bonus

🚀 Layer 2 Scaling Solutions

  • Overview: Explore Layer 2 solutions to make your DApp more scalable and reduce transaction costs.
  • Real-World Scenario: Scale your payment platform by deploying to zkSync or Optimism, enabling fast and cheap transactions for your users.
  • Production

Tasks: Integrate your DApp with a Layer 2 solution like Arbitrum, zkSync, or Optimism to reduce gas fees and improve scalability.

// zkSync scaling solution integration example (off-chain features)
// Contract deployment and execution would follow similar logic as other examples.

🏦 Decentralized Finance (DeFi) Lending Platform

  • Overview: Implement a DeFi lending platform where users can deposit and borrow assets with interest rates determined algorithmically.
  • Real-World Scenario: Build a lending protocol similar to Aave or Compound, allowing users to deposit assets as collateral and borrow other assets.
  • Production

Tasks: Implement the borrowing and repaying logic for a decentralized lending platform.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract LendingPlatform {
// Mapping to store the deposited funds of each user by their address
mapping(address => uint256) public deposits;

// Mapping to store the amount each user has borrowed
mapping(address => uint256) public borrowed;

/**
* @dev Allows users to deposit ETH into the lending platform.
* The deposited amount is stored in the `deposits` mapping.
* The deposit amount is added to the user's balance.
*/
function deposit() public payable {
deposits[msg.sender] += msg.value;
}

/**
* @dev Allows users to borrow funds from the platform if they have sufficient collateral.
* The borrower must have deposited at least the amount they wish to borrow.
* The borrowed amount is transferred to the user and deducted from their available balance.
* @param _amount The amount of ETH the user wants to borrow.
*/
function borrow(uint256 _amount) public {
// Ensure the user has sufficient deposits to cover the amount they want to borrow
require(deposits[msg.sender] >= _amount, "Insufficient collateral");

// Increase the borrowed amount for the user
borrowed[msg.sender] += _amount;

// Transfer the borrowed amount to the user's address
payable(msg.sender).transfer(_amount);
}

/**
* @dev Allows users to repay their borrowed funds.
* The repayment amount is deducted from the user's borrowed balance.
* @param _amount The amount of ETH the user is repaying.
*/
function repay(uint256 _amount) public payable {
// Ensure the user is not repaying more than the borrowed amount
require(borrowed[msg.sender] >= _amount, "Repaying more than borrowed");

// Deduct the repayment amount from the borrowed balance
borrowed[msg.sender] -= _amount;
}
}

Mappings:

  • deposits: Tracks how much ETH each user has deposited into the lending platform.
  • borrowed: Tracks how much ETH each user has borrowed.

deposit Function:

  • Users can deposit ETH into the platform.
  • The deposited ETH is added to their balance in the deposits mapping.

borrow Function:

  • Users can borrow funds from the platform if they have enough collateral (the amount of ETH they have already deposited).
  • The borrowed amount is transferred to the user and recorded in the borrowed mapping.
  • The collateral system ensures that users cannot borrow more than they have deposited.

repay Function:

  • Users can repay the amount they have borrowed.
  • This decreases the borrowed amount in the mapping. However, no interest is applied in this simplified version.

This contract represents the core of a decentralized lending platform similar to platforms like Aave or Compound, where users can deposit their cryptocurrency as collateral to borrow other assets.

For example, a user may deposit 1 ETH and borrow up to 1 ETH in another token, allowing them to leverage their assets without selling them. Upon borrowing, the user will be responsible for repaying the loan, potentially with interest (though this contract does not include interest logic). The decentralized nature of this system enables trustless peer-to-peer lending without a traditional intermediary like a bank.

Conclusion

By following this journey, you’ve built real-world decentralized applications (DApps) from the ground up using Solidity. From creating simple token contracts to implementing complex decentralized finance (DeFi) systems, you’ve gained a comprehensive understanding of smart contract development and deployed solutions on Ethereum’s mainnet. These projects give you practical experience and prepare you to tackle real-world problems in the growing blockchain ecosystem.

Good luck, and happy coding! 🚀

--

--