🔒 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:
| Parameter | Value |
|---|---|
| shares | 1000 |
| name | "Pat" |
| salary | 50000 |
| idNumber | 112358132134 |
// 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:
| Parameter | Value |
|---|---|
| _IDNUMBER | 55555 |
| _MANAGERID | 12345 |
| _HOURLYRATE | 20 |
| Parameter | Value |
|---|---|
| _IDNUMBER | 54321 |
| _MANAGERID | 11111 |
| _ANNUALSALARY | 200000 |
| Parameter | Value |
|---|---|
| _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!


Posting Komentar