Base Learn

Base Learn - Panduan Lengkap Tutorial Solidity

Base Learn Tutorial

Panduan Lengkap untuk Menyelesaikan Semua Tugas di Base Learn


📝 Petunjuk Umum: Setelah selesai melakukan deploy, salin alamat kontraknya. Lalu kunjungi alamat yang diberikan, scroll ke bawah, hubungkan wallet, dan tempelkan alamat kontrak tersebut ke dalam form yang tersedia.

⚠️ CATATAN PENTING - KEAMANAN!

🔒 GUNAKAN WALLET BARU UNTUK KEAMANAN karena ini hanya untuk mendapatkan ROLE DISCORD saja. Buat wallet khusus untuk testnet dan jangan gunakan wallet utama yang berisi aset berharga!


Mengapa perlu wallet baru?

  • ✅ Testnet deployment = Praktik keamanan yang baik
  • ✅ Isolasi dari wallet utama dengan aset berharga
  • ✅ Mengurangi risiko phishing/human error
  • ✅ Khusus untuk keperluan Base Learn Guild

1️⃣ Deployment Exercise

📖 Lihat Tutorial - > Submit Contract

📋 Instruksi:

Deploy smart contract BasicMath yang berisi fungsi matematika dasar dengan penanganan overflow/underflow.

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

contract BasicMath {

    function adder(uint _a, uint _b) public pure returns (uint sum, bool error) {
        // Gunakan `unchecked` untuk memungkinkan overflow
        unchecked {
            uint c = _a + _b;
            // Jika hasil penjumlahan lebih kecil dari _a, berarti terjadi overflow
            if (c < _a) {
                return (0, true);
            }
            return (c, false);
        }
    }

    function subtractor(uint _a, uint _b) public pure returns (uint difference, bool error) {
        // Cek secara manual apakah akan terjadi underflow (saat _a lebih kecil dari _b)
        if (_a < _b) {
            return (0, true);
        }
        
        // Gunakan `unchecked` untuk menghindari underflow check Solidity yang akan menghentikan transaksi
        unchecked {
            uint c = _a - _b;
            return (c, false);
        }
    }
}

2️⃣ Control Structures Exercise

📖 Lihat Tutorial -> Submit Contract

📋 Instruksi:

Implementasi control structures dengan fungsi FizzBuzz dan sistem notifikasi berdasarkan waktu.

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

// Definisi custom error untuk fungsi doNotDisturb
error AfterHours(uint timeProvided);

contract ControlStructures {

    function fizzBuzz(uint _number) public pure returns (string memory) {
        // Cek kondisi paling spesifik terlebih dahulu: habis dibagi 3 dan 5
        if (_number % 3 == 0 && _number % 5 == 0) {
            return "FizzBuzz";
        } 
        // Cek kondisi habis dibagi 3
        else if (_number % 3 == 0) {
            return "Fizz";
        } 
        // Cek kondisi habis dibagi 5
        else if (_number % 5 == 0) {
            return "Buzz";
        } 
        // Jika tidak ada kondisi di atas yang terpenuhi
        else {
            return "Splat";
        }
    }

    function doNotDisturb(uint _time) public pure returns (string memory) {
        // Jika _time >= 2400, trigger panic (internal error)
        if (_time >= 2400) {
            assert(_time < 2400); 
        }

        // Jika _time > 2200 atau < 800, revert dengan custom error
        if (_time > 2200 || _time < 800) {
            revert AfterHours({ timeProvided: _time });
        }
        
        // Jika _time di antara 1200 dan 1259, revert dengan pesan
        if (_time >= 1200 && _time <= 1259) {
            revert("At lunch!");
        }
        
        // Cek kondisi waktu lainnya
        if (_time >= 800 && _time <= 1199) {
            return "Morning!";
        } else if (_time >= 1300 && _time <= 1799) {
            return "Afternoon!";
        } else if (_time >= 1800 && _time <= 2200) {
            return "Evening!";
        }
        
        // Ini adalah fallback jika tidak ada kondisi yang terpenuhi,
        // meskipun logika di atas seharusnya sudah mencakup semua kemungkinan.
        return "";
    }
}

3️⃣ Storage Exercise

📖 Lihat Tutorial - Submit Contract

📋 Instruksi:

Deploy dengan constructor parameters berikut:

ParameterValue
shares1000
name"Pat"
salary50000
idNumber112358132134
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract EmployeeStorage {
    // Custom error for too many shares
    error TooManyShares(uint256 totalShares);
    
    // State variables optimized for storage packing
    // Slot 0: shares (uint16) + salary (uint32) = 48 bits total
    uint16 private shares;      // Max 65,535 (enough for max 5,000 shares)
    uint32 private salary;      // Max 4.2 billion (enough for max 1,000,000)
    
    // Slot 1: name (string) - dynamic size, takes full slot
    string public name;
    
    // Slot 2: idNumber (uint256) - takes full slot  
    uint256 public idNumber;
    
    constructor(
        uint16 _shares,
        string memory _name, 
        uint32 _salary,
        uint256 _idNumber
    ) {
        shares = _shares;
        name = _name;
        salary = _salary;
        idNumber = _idNumber;
    }
    
    // View functions for private variables
    function viewSalary() public view returns (uint32) {
        return salary;
    }
    
    function viewShares() public view returns (uint16) {
        return shares;
    }
    
    // Grant shares function with validation
    function grantShares(uint16 _newShares) public {
        // Check if _newShares itself is greater than 5000
        if (_newShares > 5000) {
            revert("Too many shares");
        }
        
        uint256 totalShares = uint256(shares) + uint256(_newShares);
        
        // Check if total would exceed 5000
        if (totalShares > 5000) {
            revert TooManyShares(totalShares);
        }
        
        shares += _newShares;
    }
    
    /**
    * Do not modify this function.  It is used to enable the unit test for this pin
    * to check whether or not you have configured your storage variables to make
    * use of packing.
    *
    * If you wish to cheat, simply modify this function to always return `0`
    * I'm not your boss ¯\_(ツ)_/¯
    *
    * Fair warning though, if you do cheat, it will be on the blockchain having been
    * deployed by your wallet....FOREVER!
    */
    function checkForPacking(uint _slot) public view returns (uint r) {
        assembly {
            r := sload (_slot)
        }
    }

    /**
    * Warning: Anyone can use this function at any time!
    */
    function debugResetShares() public {
        shares = 1000;
    }
}

4️⃣ Arrays Exercise

📖 Lihat Tutorial - > Submit Contract

📋 Instruksi:

Implementasi operasi array dengan filtering timestamp setelah Y2K (Unix timestamp: 946702800).

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

contract ArraysExercise {
    uint[] public numbers = [1,2,3,4,5,6,7,8,9,10];
    address[] public senders;
    uint[] public timestamps;

    // Return the complete numbers array
    function getNumbers() public view returns (uint[] memory) {
        return numbers;
    }

    // Reset numbers array to initial value (1-10)
    // Using more gas-efficient approach without .push()
    function resetNumbers() public {
        // Clear the array first
        delete numbers;
        // Recreate with initial values
        numbers = [1,2,3,4,5,6,7,8,9,10];
    }

    // Append array to existing numbers array
    function appendToNumbers(uint[] calldata _toAppend) public {
        for (uint i = 0; i < _toAppend.length; i++) {
            numbers.push(_toAppend[i]);
        }
    }

    // Save timestamp with caller address
    function saveTimestamp(uint _unixTimestamp) public {
        senders.push(msg.sender);
        timestamps.push(_unixTimestamp);
    }

    // Filter timestamps after Y2K (January 1, 2000, 12:00am)
    // Unix timestamp: 946702800
    function afterY2K() public view returns (uint[] memory, address[] memory) {
        // First, count how many timestamps are after Y2K
        uint count = 0;
        for (uint i = 0; i < timestamps.length; i++) {
            if (timestamps[i] > 946702800) {
                count++;
            }
        }

        // Create arrays with the correct size
        uint[] memory filteredTimestamps = new uint[](count);
        address[] memory filteredSenders = new address[](count);

        // Fill the arrays with filtered data
        uint index = 0;
        for (uint i = 0; i < timestamps.length; i++) {
            if (timestamps[i] > 946702800) {
                filteredTimestamps[index] = timestamps[i];
                filteredSenders[index] = senders[i];
                index++;
            }
        }

        return (filteredTimestamps, filteredSenders);
    }

    // Reset senders array
    function resetSenders() public {
        delete senders;
    }

    // Reset timestamps array
    function resetTimestamps() public {
        delete timestamps;
    }
}

5️⃣ Mappings Exercise

📖 Lihat Tutorial -> Submit Contract

📋 Instruksi:

Sistem manajemen record favorit dengan mapping nested untuk tracking preferensi user.

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

contract FavoriteRecords {
    // Custom error
    error NotApproved(string albumName);
    
    // State variables
    mapping(string => bool) public approvedRecords;
    mapping(address => mapping(string => bool)) public userFavorites;
    
    // Array to keep track of approved record names for retrieval
    string[] private approvedRecordsList;
    
    constructor() {
        // Load approved records
        string[9] memory albums = [
            "Thriller",
            "Back in Black", 
            "The Bodyguard",
            "The Dark Side of the Moon",
            "Their Greatest Hits (1971-1975)",
            "Hotel California",
            "Come On Over",
            "Rumours",
            "Saturday Night Fever"
        ];
        
        for (uint i = 0; i < albums.length; i++) {
            approvedRecords[albums[i]] = true;
            approvedRecordsList.push(albums[i]);
        }
    }
    
    // Get all approved records
    function getApprovedRecords() public view returns (string[] memory) {
        return approvedRecordsList;
    }
    
    // Add record to user's favorites
    function addRecord(string memory albumName) public {
        if (!approvedRecords[albumName]) {
            revert NotApproved(albumName);
        }
        userFavorites[msg.sender][albumName] = true;
    }
    
    // Get user's favorite records
    function getUserFavorites(address user) public view returns (string[] memory) {
        string[] memory favorites = new string[](approvedRecordsList.length);
        uint count = 0;
        
        for (uint i = 0; i < approvedRecordsList.length; i++) {
            if (userFavorites[user][approvedRecordsList[i]]) {
                favorites[count] = approvedRecordsList[i];
                count++;
            }
        }
        
        // Create array with exact size
        string[] memory result = new string[](count);
        for (uint i = 0; i < count; i++) {
            result[i] = favorites[i];
        }
        
        return result;
    }
    
    // Reset user's favorites
    function resetUserFavorites() public {
        for (uint i = 0; i < approvedRecordsList.length; i++) {
            userFavorites[msg.sender][approvedRecordsList[i]] = false;
        }
    }
}

6️⃣ Structs Exercise

📖 Lihat Tutorial -> Submit Contract

📋 Instruksi:

Sistem manajemen garage dengan struct Car dan operasi CRUD lengkap.

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

contract GarageManager {
    
    // Custom error for invalid car index
    error BadCarIndex(uint256 index);
    
    // Car struct with required properties
    struct Car {
        string make;
        string model;
        string color;
        uint256 numberOfDoors;
    }
    
    // Public mapping to store cars by owner address
    mapping(address => Car[]) public garage;
    
    // Add a car to the sender's garage
    function addCar(
        string memory _make,
        string memory _model,
        string memory _color,
        uint256 _numberOfDoors
    ) public {
        Car memory newCar = Car({
            make: _make,
            model: _model,
            color: _color,
            numberOfDoors: _numberOfDoors
        });
        
        garage[msg.sender].push(newCar);
    }
    
    // Get all cars owned by the calling user
    function getMyCars() public view returns (Car[] memory) {
        return garage[msg.sender];
    }
    
    // Get all cars for any given address
    function getUserCars(address _user) public view returns (Car[] memory) {
        return garage[_user];
    }
    
    // Update a car at specific index for the sender
    function updateCar(
        uint256 _index,
        string memory _make,
        string memory _model,
        string memory _color,
        uint256 _numberOfDoors
    ) public {
        // Check if sender has a car at that index
        if (_index >= garage[msg.sender].length) {
            revert BadCarIndex(_index);
        }
        
        // Update the car properties
        garage[msg.sender][_index] = Car({
            make: _make,
            model: _model,
            color: _color,
            numberOfDoors: _numberOfDoors
        });
    }
    
    // Reset the sender's garage (delete all cars)
    function resetMyGarage() public {
        delete garage[msg.sender];
    }
}

7️⃣ Inheritance Exercise

📖 Lihat Tutorial -> Submit Contract

📋 Instruksi Deployment:

⚠️ PENTING! Deploy dalam urutan berikut dan simpan Baik-baik alamat Contractnya:
1. Deploy Salesperson:
ParameterValue
_IDNUMBER55555
_MANAGERID12345
_HOURLYRATE20
2. Deploy EngineeringManager:
ParameterValue
_IDNUMBER54321
_MANAGERID11111
_ANNUALSALARY200000
3. Deploy InheritanceSubmission:
ParameterValue
_SALESPERSON[Alamat Contract Salesperson]
_ENGINEERINGMANAGER[Alamat EngineeringManager]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Abstract base contract
abstract contract Employee {
    uint public idNumber;
    uint public managerId;
    
    constructor(uint _idNumber, uint _managerId) {
        idNumber = _idNumber;
        managerId = _managerId;
    }
    
    function getAnnualCost() public view virtual returns (uint);
}

// Salaried employee contract
contract Salaried is Employee {
    uint public annualSalary;
    
    constructor(uint _idNumber, uint _managerId, uint _annualSalary) 
        Employee(_idNumber, _managerId) {
        annualSalary = _annualSalary;
    }
    
    function getAnnualCost() public view override returns (uint) {
        return annualSalary;
    }
}

// Hourly employee contract
contract Hourly is Employee {
    uint public hourlyRate;
    
    constructor(uint _idNumber, uint _managerId, uint _hourlyRate) 
        Employee(_idNumber, _managerId) {
        hourlyRate = _hourlyRate;
    }
    
    function getAnnualCost() public view override returns (uint) {
        return hourlyRate * 2080; // 2080 hours per year
    }
}

// Manager contract
contract Manager {
    uint[] public employeeIds;
    
    function addReport(uint _employeeId) public {
        employeeIds.push(_employeeId);
    }
    
    function resetReports() public {
        delete employeeIds;
    }
    
    function getEmployeeIds() public view returns (uint[] memory) {
        return employeeIds;
    }
}

// Salesperson contract (inherits from Hourly)
contract Salesperson is Hourly {
    constructor(uint _idNumber, uint _managerId, uint _hourlyRate) 
        Hourly(_idNumber, _managerId, _hourlyRate) {
    }
}

// Engineering Manager contract (multiple inheritance)
contract EngineeringManager is Salaried, Manager {
    constructor(uint _idNumber, uint _managerId, uint _annualSalary) 
        Salaried(_idNumber, _managerId, _annualSalary) {
    }
}

// Submission contract
contract InheritanceSubmission {
    address public salesPerson;
    address public engineeringManager;

    constructor(address _salesPerson, address _engineeringManager) {
        salesPerson = _salesPerson;
        engineeringManager = _engineeringManager;
    }
}

8️⃣ Imports Exercise

📖 Lihat Tutorial -> Submit Contract

📋 Instruksi:

Implementasi library dan import dengan SillyStringUtils untuk manipulasi string dan struktur Haiku.

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

// Import the SillyStringUtils library
library SillyStringUtils {
    struct Haiku {
        string line1;
        string line2;
        string line3;
    }

    function shruggie(string memory _input) internal pure returns (string memory) {
        return string.concat(_input, unicode" 🤷");
    }
}

contract ImportsExercise {
    // Using the library
    using SillyStringUtils for string;
    
    // Public instance of Haiku
    SillyStringUtils.Haiku public haiku;
    
    // Save Haiku function
    function saveHaiku(
        string memory _line1, 
        string memory _line2, 
        string memory _line3
    ) public {
        haiku.line1 = _line1;
        haiku.line2 = _line2;
        haiku.line3 = _line3;
    }
    
    // Get Haiku function - returns the complete Haiku struct
    function getHaiku() public view returns (SillyStringUtils.Haiku memory) {
        return haiku;
    }
    
    // Shruggie Haiku function - adds 🤷 to line3 without modifying original
    function shruggieHaiku() public view returns (SillyStringUtils.Haiku memory) {
        SillyStringUtils.Haiku memory modifiedHaiku = SillyStringUtils.Haiku({
            line1: haiku.line1,
            line2: haiku.line2,
            line3: SillyStringUtils.shruggie(haiku.line3)
        });
        return modifiedHaiku;
    }
}

9️⃣ Error Triage Exercise

📖 Lihat Tutorial -> Submit Contract

📋 Instruksi:

Debug dan perbaiki error pada smart contract dengan penanganan underflow, overflow, dan array bounds.

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

contract ErrorTriageExercise {
    /**
     * Finds the difference between each uint with it's neighbor (a to b, b to c, etc.)
     * and returns a uint array with the absolute integer difference of each pairing.
     */
    function diffWithNeighbor(
        uint _a,
        uint _b,
        uint _c,
        uint _d
    ) public pure returns (uint[] memory) {
        uint[] memory results = new uint[](3);

        // Fix: Calculate absolute difference to avoid underflow
        results[0] = _a > _b ? _a - _b : _b - _a;
        results[1] = _b > _c ? _b - _c : _c - _b;
        results[2] = _c > _d ? _c - _d : _d - _c;

        return results;
    }

    /**
     * Changes the _base by the value of _modifier.  Base is always >= 1000.  Modifiers can be
     * between positive and negative 100;
     */
    function applyModifier(
        uint _base,
        int _modifier
    ) public pure returns (uint) {
        // Fix: Handle negative modifiers properly to avoid underflow
        if (_modifier >= 0) {
            return _base + uint(_modifier);
        } else {
            uint absModifier = uint(-_modifier);
            require(_base >= absModifier, "Result would be negative");
            return _base - absModifier;
        }
    }

    /**
     * Pop the last element from the supplied array, and return the popped
     * value (unlike the built-in function)
     */
    uint[] arr;

    function popWithReturn() public returns (uint) {
        require(arr.length > 0, "Array is empty");
        
        // Fix: Store the value before popping, then actually remove the element
        uint lastElement = arr[arr.length - 1];
        arr.pop(); // This actually removes the last element
        return lastElement;
    }

    // The utility functions below are working as expected
    function addToArr(uint _num) public {
        arr.push(_num);
    }

    function getArr() public view returns (uint[] memory) {
        return arr;
    }

    function resetArr() public {
        delete arr;
    }
}

🔟 New Keyword Exercise

📖 Lihat Tutorial -> Submit Contract

📋 Instruksi:

Setelah menekan Compile, buka dropdown Contract, pilih AddressBookFactory lalu Deploy.

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

import "@openzeppelin/contracts/access/Ownable.sol";

contract AddressBook is Ownable {
    struct Contact {
        uint256 id;
        string firstName;
        string lastName;
        uint256[] phoneNumbers;
    }
    
    mapping(uint256 => Contact) public contacts;
    uint256[] public contactIds;
    uint256 private nextId = 1;
    
    error ContactNotFound(uint256 id);
    
    // Constructor with initial owner parameter
    constructor(address _initialOwner) Ownable(_initialOwner) {}
    
    function addContact(
        string memory _firstName,
        string memory _lastName,
        uint256[] memory _phoneNumbers
    ) external onlyOwner {
        uint256 contactId = nextId++;
        contacts[contactId] = Contact(contactId, _firstName, _lastName, _phoneNumbers);
        contactIds.push(contactId);
    }
    
    function deleteContact(uint256 _id) external onlyOwner {
        if (contacts[_id].id == 0) {
            revert ContactNotFound(_id);
        }
        delete contacts[_id];
        
        for (uint256 i = 0; i < contactIds.length; i++) {
            if (contactIds[i] == _id) {
                contactIds[i] = contactIds[contactIds.length - 1];
                contactIds.pop();
                break;
            }
        }
    }
    
    function getContact(uint256 _id) external view returns (Contact memory) {
        if (contacts[_id].id == 0) {
            revert ContactNotFound(_id);
        }
        return contacts[_id];
    }
    
    function getAllContacts() external view returns (Contact[] memory) {
        uint256 validCount = 0;
        for (uint256 i = 0; i < contactIds.length; i++) {
            if (contacts[contactIds[i]].id != 0) {
                validCount++;
            }
        }
        
        Contact[] memory result = new Contact[](validCount);
        uint256 index = 0;
        for (uint256 i = 0; i < contactIds.length; i++) {
            if (contacts[contactIds[i]].id != 0) {
                result[index] = contacts[contactIds[i]];
                index++;
            }
        }
        return result;
    }
}

contract AddressBookFactory {
    event AddressBookDeployed(address indexed newAddress, address indexed owner);
    
    function deploy() external returns (address) {
        // Deploy new AddressBook with msg.sender as owner
        AddressBook newAddressBook = new AddressBook(msg.sender);
        
        // Emit event for verification
        emit AddressBookDeployed(address(newAddressBook), msg.sender);
        
        return address(newAddressBook);
    }
}

1️⃣1️⃣ Minimal Tokens Exercise

📖 Lihat Tutorial -> Submit Contract

📋 Instruksi:

Implementasi token sederhana dengan sistem claim dan transfer yang aman.

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

contract UnburnableToken {
    // Storage variables
    mapping(address => uint256) public balances;
    uint256 public totalSupply;
    uint256 public totalClaimed;
    
    // Mapping to track which addresses have already claimed
    mapping(address => bool) public hasClaimed;
    
    // Custom errors
    error TokensClaimed();
    error AllTokensClaimed();
    error UnsafeTransfer(address _address);
    
    // Constructor - sets total supply to 100,000,000
    constructor() {
        totalSupply = 100_000_000; // No decimals, simple integer tokens
    }
    
    // Claim function - allows claiming 1000 tokens once per address
    function claim() public {
        // Check if all tokens have been claimed
        if (totalClaimed >= totalSupply) {
            revert AllTokensClaimed();
        }
        
        // Check if this address has already claimed
        if (hasClaimed[msg.sender]) {
            revert TokensClaimed();
        }
        
        // Mark address as having claimed
        hasClaimed[msg.sender] = true;
        
        // Add 1000 tokens to balance (no decimals)
        uint256 claimAmount = 1000;
        balances[msg.sender] += claimAmount;
        totalClaimed += claimAmount;
    }
    
    // Safe transfer function with safety checks
    function safeTransfer(address _to, uint256 _amount) public {
        // Check if recipient is not zero address
        if (_to == address(0)) {
            revert UnsafeTransfer(_to);
        }
        
        // Check if recipient has balance > 0 Base Sepolia ETH
        if (_to.balance == 0) {
            revert UnsafeTransfer(_to);
        }
        
        // Check if sender has sufficient balance
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        
        // Perform transfer
        balances[msg.sender] -= _amount;
        balances[_to] += _amount;
    }
}

1️⃣2️⃣ ERC-20 Tokens Exercise

📖 Lihat Tutorial -> Submit Contract

📋 Instruksi:

Implementasi token ERC-20 dengan sistem voting berbobot dan manajemen issue.

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

contract WeightedVoting is ERC20 {
    using EnumerableSet for EnumerableSet.AddressSet;
    
    uint256 public constant maxSupply = 1000000; // 1 million tokens (no decimals for this exercise)
    
    // Custom errors
    error TokensClaimed();
    error AllTokensClaimed();
    error NoTokensHeld();
    error QuorumTooHigh(uint256 quorum);
    error AlreadyVoted();
    error VotingClosed();
    
    // Struct untuk Issue
    struct Issue {
        EnumerableSet.AddressSet voters;
        string issueDesc;
        uint256 votesFor;
        uint256 votesAgainst;
        uint256 votesAbstain;
        uint256 totalVotes;
        uint256 quorum;
        bool passed;
        bool closed;
    }
    
    // Array of Issues
    Issue[] private issues;
    
    // Enum untuk Vote
    enum Vote {
        AGAINST,
        FOR,
        ABSTAIN
    }
    
    // Mapping untuk track siapa yang sudah claim
    mapping(address => bool) public hasClaimed;
    
    constructor() ERC20("WeightedVoting", "WV") {
        // Burn zeroeth element dengan membuat issue kosong
        issues.push();
        // Initialize the first issue dengan data kosong
        issues[0].issueDesc = "";
        issues[0].quorum = 0;
    }
    
    // Override decimals untuk menggunakan 0 decimals
    function decimals() public pure override returns (uint8) {
        return 0;
    }
    
    function claim() public {
        // Check jika semua token sudah terdistribusi
        if (totalSupply() >= maxSupply) {
            revert AllTokensClaimed();
        }
        
        // Check jika wallet sudah pernah claim
        if (hasClaimed[msg.sender]) {
            revert TokensClaimed();
        }
        
        // Mint 100 tokens (tanpa decimals)
        uint256 claimAmount = 100;
        
        // Check jika minting ini akan melebihi maxSupply
        if (totalSupply() + claimAmount > maxSupply) {
            revert AllTokensClaimed();
        }
        
        // Mark sebagai sudah claim dan mint tokens
        hasClaimed[msg.sender] = true;
        _mint(msg.sender, claimAmount);
    }
    
    function createIssue(string memory _issueDesc, uint256 _quorum) external returns (uint256) {
        // Check jika user memegang token
        if (balanceOf(msg.sender) == 0) {
            revert NoTokensHeld();
        }
        
        // Check jika quorum tidak lebih besar dari total supply
        if (_quorum > totalSupply()) {
            revert QuorumTooHigh(_quorum);
        }
        
        // Buat issue baru
        issues.push();
        uint256 issueIndex = issues.length - 1;
        
        Issue storage newIssue = issues[issueIndex];
        newIssue.issueDesc = _issueDesc;
        newIssue.quorum = _quorum;
        newIssue.votesFor = 0;
        newIssue.votesAgainst = 0;
        newIssue.votesAbstain = 0;
        newIssue.totalVotes = 0;
        newIssue.passed = false;
        newIssue.closed = false;
        
        return issueIndex;
    }
    
    // Struct untuk return data dari getIssue (karena EnumerableSet tidak bisa di-return)
    struct IssueView {
        address[] voters;
        string issueDesc;
        uint256 votesFor;
        uint256 votesAgainst;
        uint256 votesAbstain;
        uint256 totalVotes;
        uint256 quorum;
        bool passed;
        bool closed;
    }
    
    function getIssue(uint256 _id) external view returns (IssueView memory) {
        require(_id < issues.length, "Issue does not exist");
        
        Issue storage issue = issues[_id];
        
        // Convert EnumerableSet ke array
        uint256 voterCount = issue.voters.length();
        address[] memory voterList = new address[](voterCount);
        
        for (uint256 i = 0; i < voterCount; i++) {
            voterList[i] = issue.voters.at(i);
        }
        
        return IssueView({
            voters: voterList,
            issueDesc: issue.issueDesc,
            votesFor: issue.votesFor,
            votesAgainst: issue.votesAgainst,
            votesAbstain: issue.votesAbstain,
            totalVotes: issue.totalVotes,
            quorum: issue.quorum,
            passed: issue.passed,
            closed: issue.closed
        });
    }
    
    function vote(uint256 _issueId, Vote _vote) public {
        require(_issueId < issues.length && _issueId > 0, "Invalid issue ID");
        
        Issue storage issue = issues[_issueId];
        
        // Check jika voting sudah ditutup
        if (issue.closed) {
            revert VotingClosed();
        }
        
        // Check jika user sudah vote
        if (issue.voters.contains(msg.sender)) {
            revert AlreadyVoted();
        }
        
        // User harus memegang token untuk vote
        uint256 userTokens = balanceOf(msg.sender);
        if (userTokens == 0) {
            revert NoTokensHeld();
        }
        
        // Add voter ke set
        issue.voters.add(msg.sender);
        
        // Add votes berdasarkan pilihan
        if (_vote == Vote.FOR) {
            issue.votesFor += userTokens;
        } else if (_vote == Vote.AGAINST) {
            issue.votesAgainst += userTokens;
        } else {
            issue.votesAbstain += userTokens;
        }
        
        issue.totalVotes += userTokens;
        
        // Check jika quorum tercapai
        if (issue.totalVotes >= issue.quorum) {
            issue.closed = true;
            
            // Check jika votes FOR lebih banyak dari AGAINST
            if (issue.votesFor > issue.votesAgainst) {
                issue.passed = true;
            }
        }
    }
}

1️⃣3️⃣ ERC-721 Tokens Exercise

📖 Lihat Tutorial -> Submit Contract

📋 Instruksi:

Implementasi NFT ERC-721 untuk Haiku dengan sistem sharing dan uniqueness validation.

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract HaikuNFT is ERC721 {
    // Custom errors
    error HaikuNotUnique();
    error NotYourHaiku(uint256 haikuId);
    error NoHaikusShared();

    // Struct untuk menyimpan data haiku
    struct Haiku {
        address author;
        string line1;
        string line2;
        string line3;
    }

    // Array public untuk menyimpan semua haiku
    Haiku[] public haikus;

    // Mapping untuk menyimpan haiku yang dibagikan dari address ke haiku ID
    mapping(address => uint256[]) public sharedHaikus;

    // Counter untuk tracking total haiku dan sebagai ID berikutnya
    uint256 public counter = 1;

    // Mapping untuk tracking line yang sudah digunakan
    mapping(string => bool) private usedLines;

    constructor() ERC721("HaikuNFT", "HAIKU") {
        // Constructor kosong, ERC721 sudah menginisialisasi nama dan symbol
    }

    // Function untuk mint haiku baru
    function mintHaiku(
        string memory _line1,
        string memory _line2,
        string memory _line3
    ) external {
        // Cek apakah salah satu line sudah pernah digunakan
        if (
            usedLines[_line1] ||
            usedLines[_line2] ||
            usedLines[_line3]
        ) {
            revert HaikuNotUnique();
        }

        // Tandai lines sebagai sudah digunakan
        usedLines[_line1] = true;
        usedLines[_line2] = true;
        usedLines[_line3] = true;

        // Buat haiku baru
        Haiku memory newHaiku = Haiku({
            author: msg.sender,
            line1: _line1,
            line2: _line2,
            line3: _line3
        });

        // Tambahkan ke array haikus
        haikus.push(newHaiku);

        // Mint NFT dengan counter sebagai tokenId
        _mint(msg.sender, counter);

        // Increment counter untuk ID berikutnya
        counter++;
    }

    // Function untuk share haiku ke address lain
    function shareHaiku(address _to, uint256 _haikuId) public {
        // Cek apakah sender adalah owner dari haiku NFT
        if (ownerOf(_haikuId) != msg.sender) {
            revert NotYourHaiku(_haikuId);
        }

        // Tambahkan haiku ID ke shared haikus dari address tujuan
        sharedHaikus[_to].push(_haikuId);
    }

    // Function untuk mendapatkan semua haiku yang dibagikan ke caller
    function getMySharedHaikus() public view returns (Haiku[] memory) {
        uint256[] memory mySharedIds = sharedHaikus[msg.sender];
        
        // Cek apakah ada haiku yang dibagikan
        if (mySharedIds.length == 0) {
            revert NoHaikusShared();
        }

        // Buat array hasil dengan ukuran sesuai jumlah shared haikus
        Haiku[] memory result = new Haiku[](mySharedIds.length);
        
        // Loop dan ambil data haiku berdasarkan ID
        for (uint256 i = 0; i < mySharedIds.length; i++) {
            // Karena tokenId dimulai dari 1, tapi array index dari 0
            result[i] = haikus[mySharedIds[i] - 1];
        }

        return result;
    }

    // Helper function untuk mendapatkan total haiku yang sudah di-mint
    function getTotalHaikus() public view returns (uint256) {
        return haikus.length;
    }

    // Helper function untuk mendapatkan haiku berdasarkan ID
    function getHaiku(uint256 _haikuId) public view returns (Haiku memory) {
        require(_haikuId > 0 && _haikuId < counter, "Invalid haiku ID");
        return haikus[_haikuId - 1];
    }
}

🎉 Selamat!

Anda telah menyelesaikan semua 13 tutorial Base Learn.

💡 Tips: Gunakan tombol 📋 Copy Code untuk menyalin code dengan mudah!


🔴 Untuk cara deploy, kalian bisa menonton video di bawah ini.:

Posting Komentar

Arsip