๐ช Token Standards with Arbitrum Stylus
๐ Townhall 3: Token Development & Mini Hackathon Kickoff
Pelajari cara membuat ERC-20 token dan ERC-721 NFT menggunakan Rust di Arbitrum Stylus. Deploy token kamu sendiri di testnet dan mulai hackathon project!
๐ฏ Apa yang Akan Dipelajariโ
Di sesi ini kita akan:
- Pengembangan Token ERC-20 - Membuat fungible token sendiri (seperti USDC, DAI)
- Membuat NFT ERC-721 - Membuat koleksi NFT (seperti Bored Ape, CryptoPunks)
- Praktik Deployment - Deploy ke Arbitrum Sepolia testnet
- Memulai Mini Hackathon - Pembentukan tim dan ide proyek
Prasyarat:
- Sudah menyelesaikan Modul 8 (Rust Fundamentals)
- Sudah menyelesaikan Modul 9 (Stylus SDK Simple Start)
- Memiliki testnet ETH di Arbitrum Sepolia (Faucet)
๐ Bagian1: Understanding Token Standardsโ
Apa itu ERC-20?โ
ERC-20 adalah standard untuk fungible tokens - setiap token identik dan bisa ditukar 1:1.
Contoh di Dunia Nyata:
- USDC (stablecoin)
- UNI (token governance Uniswap)
- LINK (token oracle Chainlink)
Fungsi Utama:
// Total supply
totalSupply() โ uint256
// Balance query
balanceOf(address) โ uint256
// Transfer
transfer(address to, uint256 amount) โ bool
// Approve & transferFrom (untuk DEX, dll)
approve(address spender, uint256 amount) โ bool
transferFrom(address from, address to, uint256 amount) โ bool
allowance(address owner, address spender) โ uint256
Apa itu ERC-721?โ
ERC-721 adalah standard untuk non-fungible tokens (NFTs) - setiap token unik dengan ID berbeda.
Contoh di Dunia Nyata:
- Bored Ape Yacht Club (gambar profil)
- Azuki (NFT anime)
- ENS Domains (nama domain)
Fungsi Utama:
// Balance query
balanceOf(address owner) โ uint256
// Ownership
ownerOf(uint256 tokenId) โ address
// Transfer
transferFrom(address from, address to, uint256 tokenId)
safeTransferFrom(address from, address to, uint256 tokenId)
// Approval
approve(address to, uint256 tokenId)
setApprovalForAll(address operator, bool approved)
๐ช Bagian 2: Pengembangan Token ERC-20 dengan Rustโ
Langkah 1: Setup Projectโ
cargo stylus new erc20-token
cd erc20-token
Langkah2: Update Cargo.tomlโ
[package]
name = "erc20-token"
version = "0.1.0"
edition = "2021"
[dependencies]
alloy-primitives = "=0.8.20"
alloy-sol-types = "=0.8.20"
stylus-sdk = "=0.9.0"
mini-alloc = "0.4.2"
ruint = "=1.15.0"
[dev-dependencies]
tokio = { version = "1.12.0", features = ["full"] }
ethers = "2.0"
eyre = "0.6.8"
[features]
export-abi = ["stylus-sdk/export-abi"]
[lib]
crate-type = ["lib", "cdylib"]
[profile.release]
codegen-units = 1
strip = true
lto = true
panic = "abort"
opt-level = "s"
Penjelasan Dependencies:
alloy-primitives&alloy-sol-typesv0.8.20: Ethereum types & Solidity ABI encodingstylus-sdkv0.9.0: Arbitrum Stylus SDK (stable version, AVOID 0.10.0 yang ada known issues)ruintv1.15.0: PENTING! Di-lock ke v1.15.0 untuk prevent compatibility issues dengan alloy-primitivesmini-alloc: Memory allocator untuk WASM environment
Jika kamu menggunakan versi yang berbeda, kemungkinan besar akan ada compile error seperti:
BYTES must be equal to Self::BYTES(ruint version mismatch)evaluation panickederrors- ABI encoding issues
Solusi: Gunakan versi EXACT seperti di atas (perhatikan tanda = di depan version number)!
Langkah3: Create rust-toolchain.tomlโ
[toolchain]
channel = "1.88.0"
targets = ["wasm32-unknown-unknown"]
Kenapa perlu ini?
- Lock Rust version ke 1.88.0 untuk consistency
- Target
wasm32-unknown-unknownuntuk compile ke WebAssembly - Stylus contracts run on WASM runtime!
Langkah4: Create Stylus.tomlโ
[workspace]
[workspace.networks]
[contract]
Kenapa perlu ini?
- Konfigurasi deployment untuk Arbitrum Stylus
- Bisa diisi dengan custom network settings (opsional)
Langkah5: Create src/main.rsโ
#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
#[cfg(not(any(test, feature = "export-abi")))]
#[unsafe(no_mangle)]
pub extern "C" fn main() {}
#[cfg(feature = "export-abi")]
fn main() {
erc20_token::print_from_args();
}
Kenapa perlu ini?
- Entry point untuk WASM binary
- Support ABI export via
cargo stylus export-abi
Langkah6: Implement ERC-20 Token (src/lib.rs)โ
#![cfg_attr(not(feature = "export-abi"), no_main)]
extern crate alloc;
use stylus_sdk::prelude::*;
use stylus_sdk::alloy_primitives::{Address, U256, U8};
use stylus_sdk::alloy_sol_types::{sol, SolError};
// Define events using Solidity ABI
sol! {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// Define errors using Solidity ABI
sol! {
error InsufficientBalance(uint256 balance, uint256 required);
error InsufficientAllowance(uint256 allowance, uint256 required);
}
// Storage definition
sol_storage! {
#[entrypoint]
pub struct ERC20Token {
// Token metadata
string name;
string symbol;
uint8 decimals;
// Balances
mapping(address => uint256) balances;
// Allowances (owner => spender => amount)
mapping(address => mapping(address => uint256)) allowances;
// Total supply
uint256 total_supply;
}
}
#[public]
impl ERC20Token {
/// Initialize token with name, symbol, and initial supply
pub fn init(&mut self,
token_name: String,
token_symbol: String,
initial_supply: U256
) -> Result<(), Vec<u8>> {
// Set metadata
self.name.set_str(&token_name);
self.symbol.set_str(&token_symbol);
self.decimals.set(U8::from(18));
// Mint initial supply to deployer
let deployer = self.vm().msg_sender();
self.balances.setter(deployer).set(initial_supply);
self.total_supply.set(initial_supply);
// Emit Transfer event from zero address
log(self.vm(), Transfer {
from: Address::ZERO,
to: deployer,
value: initial_supply,
});
Ok(())
}
/// Get token name
pub fn name(&self) -> String {
self.name.get_string()
}
/// Get token symbol
pub fn symbol(&self) -> String {
self.symbol.get_string()
}
/// Get decimals (usually 18)
pub fn decimals(&self) -> U8 {
self.decimals.get()
}
/// Get total supply
pub fn total_supply(&self) -> U256 {
self.total_supply.get()
}
/// Get balance of an address
pub fn balance_of(&self, account: Address) -> U256 {
self.balances.get(account)
}
/// Transfer tokens to another address
pub fn transfer(&mut self, to: Address, amount: U256) -> Result<bool, Vec<u8>> {
let from = self.vm().msg_sender();
self._transfer(from, to, amount)?;
Ok(true)
}
/// Approve spender to spend tokens on your behalf
pub fn approve(&mut self, spender: Address, amount: U256) -> Result<bool, Vec<u8>> {
let owner = self.vm().msg_sender();
// Set allowance
self.allowances.setter(owner).setter(spender).set(amount);
// Emit Approval event
log(self.vm(), Approval {
owner,
spender,
value: amount,
});
Ok(true)
}
/// Get allowance
pub fn allowance(&self, owner: Address, spender: Address) -> U256 {
self.allowances.getter(owner).get(spender)
}
/// Transfer from one address to another using allowance
pub fn transfer_from(
&mut self,
from: Address,
to: Address,
amount: U256
) -> Result<bool, Vec<u8>> {
let spender = self.vm().msg_sender();
// Check allowance
let current_allowance = self.allowances.getter(from).get(spender);
if current_allowance < amount {
return Err(InsufficientAllowance {
allowance: current_allowance,
required: amount,
}.abi_encode());
}
// Decrease allowance
let new_allowance = current_allowance - amount;
self.allowances.setter(from).setter(spender).set(new_allowance);
// Transfer
self._transfer(from, to, amount)?;
Ok(true)
}
/// Internal transfer function
fn _transfer(&mut self, from: Address, to: Address, amount: U256) -> Result<(), Vec<u8>> {
// Check balance
let from_balance = self.balances.get(from);
if from_balance < amount {
return Err(InsufficientBalance {
balance: from_balance,
required: amount,
}.abi_encode());
}
// Update balances
self.balances.setter(from).set(from_balance - amount);
let to_balance = self.balances.get(to);
self.balances.setter(to).set(to_balance + amount);
// Emit Transfer event
log(self.vm(), Transfer {
from,
to,
value: amount,
});
Ok(())
}
}
Penjelasan Kode:
-
Imports:
U8untuk decimals (lebih efisien dari U256)sol!macro untuk mendefinisikan events dan errorsSolErrortrait untuk encoding error
-
Events & Errors:
- Didefinisikan menggunakan sintaks Solidity di dalam
sol!macro - Errors di-encode ke
Vec<u8>melalui.abi_encode()
- Didefinisikan menggunakan sintaks Solidity di dalam
-
Storage:
sol_storage!macro dengan atribut#[entrypoint]- Nested mapping untuk allowances (izin transfer)
-
VM Context:
self.vm().msg_sender()untuk mengakses alamat pemanggillog(self.vm(), Event { ... })untuk emit events
-
Error Handling:
- Fungsi mengembalikan
Result<T, Vec<u8>> - Errors di-encode ke format ABI untuk kompatibilitas
- Fungsi mengembalikan
-
Fungsi-Fungsi Utama:
init(): Inisialisasi token (hanya sekali)transfer(): Transfer langsungapprove(): Set allowance (izin transfer)transfer_from(): Transfer menggunakan allowance_transfer(): Logika transfer internal
Langkah7: Verify Project Structureโ
Pastikan struktur project kamu seperti ini:
erc20-token/
โโโ Cargo.toml # Dependencies
โโโ Stylus.toml # Stylus config
โโโ rust-toolchain.toml # Rust version
โโโ src/
โ โโโ lib.rs # Smart contract code
โ โโโ main.rs # Entry point
โโโ target/ # Build output (auto-generated)
Langkah8: Build & Test Contractโ
8.1. Build Contractโ
cargo stylus check
cargo stylus export-abi
8.2. Setup Environmentโ
Buat file .env:
PRIVATE_KEY=your_private_key_here
RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
Load environment variables:
# Option 1: Source the .env file (recommended)
source .env
# Option 2: Export manually
export PRIVATE_KEY=your_private_key_here
export RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
# Verify variables are loaded
echo $PRIVATE_KEY
echo $RPC_URL
PENTING:
- Jangan commit file
.envke Git (sudah ada di.gitignore) - Private key harus TANPA prefix
0x - Pastikan punya testnet ETH (Faucet link)
8.3. Deployโ
cargo stylus deploy \
--private-key=$PRIVATE_KEY \
--endpoint=$RPC_URL
Output example:
deployed code at address: 0x1234...5678
Langkah9: Initialize Your Tokenโ
Setelah deploy, panggil init() untuk set name, symbol, dan initial supply:
cast send 0x1234...5678 \
"init(string,string,uint256)" \
"MyToken" \
"MTK" \
"1000000000000000000000000" \
--rpc-url=$RPC_URL \
--private-key=$PRIVATE_KEY
(1000000 tokens dengan 18 decimals)
Langkah10: Interact with Your Tokenโ
# Check token name
cast call 0x1234...5678 "name()" --rpc-url=$RPC_URL
# Check your balance
cast call 0x1234...5678 \
"balanceOf(address)" \
YOUR_ADDRESS \
--rpc-url=$RPC_URL
# Transfer tokens
cast send 0x1234...5678 \
"transfer(address,uint256)" \
RECIPIENT_ADDRESS \
"1000000000000000000" \
--rpc-url=$RPC_URL \
--private-key=$PRIVATE_KEY
๐ผ๏ธ Bagian3: ERC-721 NFT Minting Flowsโ
Langkah1: Setup Projectโ
cargo stylus new erc721-nft
cd erc721-nft
Langkah2: Update Cargo.tomlโ
(Same as ERC-20 above)
Langkah3: Implement ERC-721 NFT (src/lib.rs)โ
#![cfg_attr(not(feature = "export-abi"), no_main)]
extern crate alloc;
use stylus_sdk::prelude::*;
use stylus_sdk::alloy_primitives::{Address, U256, FixedBytes};
use stylus_sdk::alloy_sol_types::{sol, SolError};
use alloc::string::String;
// ERC-165 Interface IDs untuk proper NFT detection
const ERC165_INTERFACE_ID: u32 = 0x01ffc9a7;
const ERC721_INTERFACE_ID: u32 = 0x80ac58cd;
const ERC721_METADATA_INTERFACE_ID: u32 = 0x5b5e139f;
// Define events using Solidity ABI
sol! {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
}
// Define errors using Solidity ABI
sol! {
error TokenDoesNotExist(uint256 tokenId);
error NotOwnerOrApproved(address caller, uint256 tokenId);
error TransferToZeroAddress();
error MintToZeroAddress();
error TokenAlreadyExists(uint256 tokenId);
error InvalidRecipient(address to);
}
// Storage definition
sol_storage! {
#[entrypoint]
pub struct ERC721NFT {
// NFT metadata
string name;
string symbol;
// Token ownership: tokenId => owner address
mapping(uint256 => address) owners;
// Balance tracking: owner => token count
mapping(address => uint256) balances;
// Token approvals: tokenId => approved address
mapping(uint256 => address) token_approvals;
// Operator approvals: owner => operator => approved
mapping(address => mapping(address => bool)) operator_approvals;
// Token URIs: tokenId => URI
mapping(uint256 => string) token_uris;
// Next token ID for minting
uint256 next_token_id;
}
}
#[public]
impl ERC721NFT {
/// Initialize NFT collection with name and symbol
pub fn init(&mut self, collection_name: String, collection_symbol: String) -> Result<(), Vec<u8>> {
self.name.set_str(&collection_name);
self.symbol.set_str(&collection_symbol);
self.next_token_id.set(U256::from(1)); // Start token IDs from 1
Ok(())
}
/// Get collection name
pub fn name(&self) -> String {
self.name.get_string()
}
/// Get collection symbol
pub fn symbol(&self) -> String {
self.symbol.get_string()
}
/// Get decimals (always 0 untuk NFT)
pub fn decimals(&self) -> u8 {
0
}
/// Get balance of an address
pub fn balance_of(&self, owner: Address) -> U256 {
self.balances.get(owner)
}
/// Get owner of a token
pub fn owner_of(&self, token_id: U256) -> Result<Address, Vec<u8>> {
let owner = self.owners.get(token_id);
if owner == Address::ZERO {
return Err(TokenDoesNotExist { tokenId: token_id }.abi_encode());
}
Ok(owner)
}
/// Get token URI
pub fn token_uri(&self, token_id: U256) -> Result<String, Vec<u8>> {
// Check if token exists
let owner = self.owners.get(token_id);
if owner == Address::ZERO {
return Err(TokenDoesNotExist { tokenId: token_id }.abi_encode());
}
Ok(self.token_uris.getter(token_id).get_string())
}
/// Mint a new NFT
pub fn mint(&mut self, to: Address, uri: String) -> Result<U256, Vec<u8>> {
if to == Address::ZERO {
return Err(MintToZeroAddress {}.abi_encode());
}
let token_id = self.next_token_id.get();
// Check if token already exists (shouldn't happen with auto-increment)
if self.owners.get(token_id) != Address::ZERO {
return Err(TokenAlreadyExists { tokenId: token_id }.abi_encode());
}
// Mint the token
self.owners.setter(token_id).set(to);
self.token_uris.setter(token_id).set_str(&uri);
// Update balance
let balance = self.balances.get(to);
self.balances.setter(to).set(balance + U256::from(1));
// Increment next token ID
self.next_token_id.set(token_id + U256::from(1));
// Emit Transfer event from zero address
log(self.vm(), Transfer {
from: Address::ZERO,
to,
tokenId: token_id,
});
Ok(token_id)
}
/// Transfer NFT
pub fn transfer_from(
&mut self,
from: Address,
to: Address,
token_id: U256
) -> Result<(), ERC721Error> {
// Validate transfer
self._validate_transfer(from, to, token_id)?;
// Clear approval
self.token_approvals.setter(token_id).set(Address::ZERO);
// Update ownership
self.owners.setter(token_id).set(to);
// Update balances
let from_balance = self.balances.get(from);
self.balances.setter(from).set(from_balance - U256::from(1));
let to_balance = self.balances.get(to);
self.balances.setter(to).set(to_balance + U256::from(1));
// Emit Transfer event
evm::log(Transfer {
from,
to,
tokenId: token_id,
});
Ok(())
}
/// Approve address to transfer specific token
pub fn approve(&mut self, to: Address, token_id: U256) -> Result<(), ERC721Error> {
let owner = self.owner_of(token_id)?;
let caller = msg::sender();
// Only owner or approved operator can approve
if caller != owner && !self.is_approved_for_all(owner, caller) {
return Err(ERC721Error::NotAuthorized(NotAuthorizedError {
caller,
}));
}
self.token_approvals.setter(token_id).set(to);
evm::log(Approval {
owner,
approved: to,
tokenId: token_id,
});
Ok(())
}
/// Get approved address for token
pub fn get_approved(&self, token_id: U256) -> Address {
self.token_approvals.get(token_id)
}
/// Approve operator to transfer all tokens
pub fn set_approval_for_all(&mut self, operator: Address, approved: bool) -> Result<(), ERC721Error> {
let owner = msg::sender();
self.operator_approvals.setter(owner).setter(operator).set(approved);
evm::log(ApprovalForAll {
owner,
operator,
approved,
});
Ok(())
}
/// Check if operator is approved for all tokens
pub fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool {
self.operator_approvals.getter(owner).get(operator)
}
/// ERC-165: Check if contract supports an interface
/// PENTING untuk block explorer detection!
pub fn supports_interface(&self, interface_id: FixedBytes<4>) -> bool {
let id = u32::from_be_bytes(interface_id.0);
id == ERC165_INTERFACE_ID
|| id == ERC721_INTERFACE_ID
|| id == ERC721_METADATA_INTERFACE_ID
}
/// Total supply (number of minted tokens)
pub fn total_supply(&self) -> U256 {
let next_id = self.next_token_id.get();
if next_id > U256::from(0) {
next_id - U256::from(1)
} else {
U256::from(0)
}
}
/// Internal transfer function
fn _transfer(&mut self, from: Address, to: Address, token_id: U256) -> Result<(), Vec<u8>> {
if to == Address::ZERO {
return Err(TransferToZeroAddress {}.abi_encode());
}
let owner = self.owners.get(token_id);
if owner != from {
return Err(InvalidRecipient { to: from }.abi_encode());
}
// Clear approvals
self.token_approvals.delete(token_id);
// Update balances
let from_balance = self.balances.get(from);
self.balances.setter(from).set(from_balance - U256::from(1));
let to_balance = self.balances.get(to);
self.balances.setter(to).set(to_balance + U256::from(1));
// Update ownership
self.owners.setter(token_id).set(to);
// Emit Transfer event
log(self.vm(), Transfer {
from,
to,
tokenId: token_id,
});
Ok(())
}
/// Check if an address is approved or owner
fn _is_approved_or_owner(&self, spender: Address, token_id: U256) -> bool {
let owner = self.owners.get(token_id);
if owner == Address::ZERO {
return false;
}
spender == owner
|| self.token_approvals.get(token_id) == spender
|| self.operator_approvals.getter(owner).get(spender)
}
/// Internal function to validate transfer (DEPRECATED - use _transfer instead)
fn _validate_transfer(&self, from: Address, to: Address, token_id: U256) -> Result<(), Vec<u8>> {
// Check token exists and owner is correct
let owner = self.owner_of(token_id)?;
if owner != from {
return Err(ERC721Error::NotOwner(NotOwnerError {
caller: from,
owner,
}));
}
// Check destination is not zero address
if to == Address::ZERO {
return Err(ERC721Error::TransferToZeroAddress(TransferToZeroAddressError {}));
}
// Check caller is authorized
let caller = msg::sender();
let is_approved = self.token_approvals.get(token_id) == caller;
let is_operator = self.operator_approvals.getter(owner).get(caller);
if caller != owner && !is_approved && !is_operator {
return Err(ERC721Error::NotAuthorized(NotAuthorizedError {
caller,
}));
}
Ok(())
}
}
Langkah4: Build & Deployโ
# Build
cargo stylus check
cargo stylus export-abi
# Deploy
cargo stylus deploy \
--private-key=$PRIVATE_KEY \
--endpoint=$RPC_URL
Langkah5: Initialize Your NFT Collectionโ
# Initialize dengan nama dan symbol (2 parameters saja!)
cast send 0x5678...9abc \
"init(string,string)" \
"My NFT Collection" \
"MNFT" \
--rpc-url=$RPC_URL \
--private-key=$PRIVATE_KEY
Function init() hanya butuh 2 parameters: name dan symbol. URI di-set per token saat mint!
Langkah6: Mint NFTsโ
# Mint NFT dengan URI metadata
cast send 0x5678...9abc \
"mint(address,string)(uint256)" \
YOUR_ADDRESS \
"ipfs://QmYourMetadataHash" \
--rpc-url=$RPC_URL \
--private-key=$PRIVATE_KEY
# Check token owner (token ID 1)
cast call 0x5678...9abc \
"ownerOf(uint256)(address)" \
1 \
--rpc-url=$RPC_URL
# Check your NFT balance
cast call 0x5678...9abc \
"balanceOf(address)(uint256)" \
YOUR_ADDRESS \
--rpc-url=$RPC_URL
# Check token URI
cast call 0x5678...9abc \
"tokenURI(uint256)(string)" \
1 \
--rpc-url=$RPC_URL
# Verify ERC-721 interface support (should return true)
cast call 0x5678...9abc \
"supportsInterface(bytes4)(bool)" \
0x80ac58cd \
--rpc-url=$RPC_URL
# Actual working example:
cast send 0xda3818869bd8fb6c4ec22376f94c9035d7220fd4 \
"mint(address,string)(uint256)" \
0x67BA06dB6d9c562857BF08AB1220a16DfA455c45 \
"ipfs://QmExample123" \
--rpc-url=$RPC_URL \
--private-key=$PRIVATE_KEY
# Output:
# โ
Transaction successful
# โ
Token ID: 1
# โ
Owner: 0x67BA06dB6d9c562857BF08AB1220a16DfA455c45
๐ฏ Bagian4: Hands-On Assignmentsโ
Tugas 1: Deploy Token ERC-20 Kamuโ
Persyaratan:
- Buat token ERC-20 dengan nama dan simbol yang unik
- Set initial supply minimal 1,000,000 token
- Deploy ke Arbitrum Sepolia testnet
- Transfer minimal 100 token ke alamat teman
- Screenshot transaksi di Arbiscan
Pengumpulan:
- Repository GitHub dengan kode lengkap
- Screenshot Arbiscan (deploy + transfer)
- Alamat contract di README.md
Tugas 2: Mint Koleksi NFT Kamuโ
Persyaratan:
- Buat koleksi NFT ERC-721 dengan nama unik
- Deploy ke Arbitrum Sepolia testnet
- Mint minimal 3 NFT
- Transfer 1 NFT ke alamat teman
- Screenshot di Arbiscan
Pengumpulan:
- Repository GitHub dengan kode lengkap
- Screenshot Arbiscan (mint + transfer)
- Alamat contract di README.md
Tugas 3: Token dengan Fitur Tambahan (Bonus)โ
Tambahkan fitur ekstra ke token ERC-20 atau ERC-721 kamu:
Ide untuk ERC-20:
- Fungsi burn (menghancurkan token)
- Pause/Unpause transfer
- Batas maksimal transfer
- Whitelist/blacklist alamat
Ide untuk ERC-721:
- URI metadata per token ID
- Batas maksimal supply
- Biaya minting (payable)
- Fungsi burn
๐ Key Learnings & Best Practicesโ
Version Management adalah CRITICAL! ๐โ
ALWAYS lock exact versions:
alloy-primitives = "=0.8.20" # โ
Exact version dengan =
stylus-sdk = "=0.9.0" # โ
Locked version
ruint = "=1.15.0" # โ
CRITICAL untuk avoid conflicts
NEVER use:
alloy-primitives = "0.8" # โ Too loose!
stylus-sdk = "^0.9.0" # โ May upgrade to 0.10.0 yang broken
Kenapa penting?
- Version mismatch = compile errors yang confusing
ruintversion salah = "BYTES must be equal to Self::BYTES" error- Stability > "latest version"
Use Non-Deprecated APIs โจโ
Stylus SDK 0.8.0+ introduced new Host trait pattern:
| Old (Deprecated) โ | New (Clean) โ |
|---|---|
use stylus_sdk::msg;msg::sender() | use stylus_sdk::prelude::*;self.vm().msg_sender() |
use stylus_sdk::evm;evm::log(Event { ... }) | log(self.vm(), Event { ... }) |
msg::value() | self.vm().msg_value() |
Benefits:
- โ Zero deprecation warnings
- โ Better testability
- โ Future-proof code
- โ Cleaner architecture
ERC-165 is MANDATORY for NFTs ๐จโ
Without ERC-165:
- Block explorers detect as "ERC-20" atau generic "Contract"
- Missing NFT marketplace compatibility
- Poor UX
With ERC-165:
const ERC165_INTERFACE_ID: u32 = 0x01ffc9a7;
const ERC721_INTERFACE_ID: u32 = 0x80ac58cd;
const ERC721_METADATA_INTERFACE_ID: u32 = 0x5b5e139f;
pub fn supports_interface(&self, interface_id: FixedBytes<4>) -> bool {
let id = u32::from_be_bytes(interface_id.0);
id == ERC165_INTERFACE_ID
|| id == ERC721_INTERFACE_ID
|| id == ERC721_METADATA_INTERFACE_ID
}
Result:
- โ Properly detected as ERC-721
- โ OpenSea/Rarible compatibility
- โ Better block explorer UX
Command-Line Best Practices ๐ปโ
Always use single-line OR proper backslashes:
โ WRONG:
cast send 0x123...
"function()"
arg1
โ CORRECT:
# Option 1: Single line
cast send 0x123... "function()" arg1 --rpc-url=$RPC --private-key=$KEY
# Option 2: Backslashes
cast send 0x123... \
"function()" \
arg1 \
--rpc-url=$RPC \
--private-key=$KEY
Error Handling Pattern ๐ก๏ธโ
Use sol! macro untuk errors:
sol! {
error InsufficientBalance(uint256 balance, uint256 required);
}
// Encode and return
if balance < amount {
return Err(InsufficientBalance {
balance,
required: amount,
}.abi_encode());
}
Benefits:
- โ ABI-compatible errors
- โ Proper error messages
- โ Frontend can decode errors
- โ Better debugging
Testing Strategy ๐งชโ
Before deploy:
cargo stylus check # Compile & size check
cargo stylus export-abi # Verify ABI generation
After deploy:
# 1. Initialize
cast send <ADDRESS> "init(...)" args...
# 2. Verify initialization
cast call <ADDRESS> "name()(string)" --rpc-url=$RPC
# 3. Test core functionality
cast send <ADDRESS> "mint(...)" args...
# 4. Verify state changes
cast call <ADDRESS> "balanceOf(address)(uint256)" $YOUR_ADDR --rpc-url=$RPC
# 5. Check on Arbiscan
https://sepolia.arbiscan.io/address/<ADDRESS>
Gas Optimization Tips โฝโ
1. Use appropriate types:
uint8 decimals; // โ
Not uint256 untuk small values
uint256 balance; // โ
For large token amounts
2. Storage reads are expensive:
// โ Multiple reads
let bal1 = self.balances.get(owner);
let bal2 = self.balances.get(owner);
// โ
Single read
let balance = self.balances.get(owner);
// Use 'balance' variable multiple times
3. Batch operations:
// Consider batching if minting multiple NFTs
pub fn batch_mint(&mut self, to: Address, count: U256) -> Result<Vec<U256>, Vec<u8>>
๐ What You've Learnedโ
Setelah menyelesaikan modul ini, kamu sekarang bisa:
โ
Memahami ERC-20 dan ERC-721 token standards
โ
Implement ERC-20 token lengkap di Rust dengan clean code (zero warnings!)
โ
Implement ERC-721 NFT collection di Rust dengan ERC-165 support
โ
Handle version conflicts dan dependency management
โ
Use non-deprecated APIs (self.vm().msg_sender(), log())
โ
Deploy dan verify contracts di Arbitrum Sepolia
โ
Interact dengan tokens via cast commands
โ
Troubleshoot common errors dengan confidence
โ
Work in teams untuk hackathon projects
๐ Real Working Examplesโ
Berikut adalah contoh contracts yang BENAR-BENAR DEPLOYED dan working di Arbitrum Sepolia testnet:
ERC-20 Token Exampleโ
Contract Address: 0x7bf619a8ad20b0f44ce4bdf601f56a64679ffd28
Details:
- Name: MyToken
- Symbol: MTK
- Initial Supply: 1,000,000 MTK
- Decimals: 18
- Status: โ Deployed & Initialized
View on Arbiscan: https://sepolia.arbiscan.io/address/0x7bf619a8ad20b0f44ce4bdf601f56a64679ffd28
Transactions:
- Deploy: Block 239399093
- Init: [Transaction Hash]
- Features: transfer(), approve(), transferFrom()
ERC-721 NFT Exampleโ
Contract Address: 0xda3818869bd8fb6c4ec22376f94c9035d7220fd4
Details:
- Name: My NFT Collection
- Symbol: MNFT
- Total Minted: 1 NFT
- Status: โ Deployed, Initialized & Minted
View on Arbiscan: https://sepolia.arbiscan.io/address/0xda3818869bd8fb6c4ec22376f94c9035d7220fd4
Key Features:
- โ
ERC-165 interface detection (
supportsInterface) - โ Per-token metadata URIs
- โ Full approval system (single + operator)
- โ Auto-incrementing token IDs
Verify Interface Support:
cast call 0xda3818869bd8fb6c4ec22376f94c9035d7220fd4 \
"supportsInterface(bytes4)(bool)" \
0x80ac58cd \
--rpc-url=https://sepolia-rollup.arbitrum.io/rpc
# Returns: true โ
Check Token Owner:
cast call 0xda3818869bd8fb6c4ec22376f94c9035d7220fd4 \
"ownerOf(uint256)(address)" \
1 \
--rpc-url=https://sepolia-rollup.arbitrum.io/rpc
# Returns: 0x67BA06dB6d9c562857BF08AB1220a16DfA455c45
Code Comparison: Before vs Afterโ
BEFORE (With Issues):
// โ Old deprecated APIs
use stylus_sdk::{msg, evm};
let sender = msg::sender();
evm::log(Transfer { from, to, value });
// โ Wrong versions
stylus-sdk = "0.6.0" // Too old!
ruint = "1.17.2" // Incompatible!
// โ Missing ERC-165
// No supportsInterface() = detected as ERC-20
AFTER (Clean & Working):
// โ
New non-deprecated APIs
use stylus_sdk::prelude::*;
let sender = self.vm().msg_sender();
log(self.vm(), Transfer { from, to, value });
// โ
Locked compatible versions
stylus-sdk = "=0.9.0" // Stable!
ruint = "=1.15.0" // Compatible!
// โ
Proper ERC-165 implementation
pub fn supports_interface(&self, interface_id: FixedBytes<4>) -> bool {
// ... proper detection
}
Results:
- Compile time: 58.76s โ 5.70s (after first build)
- Warnings: 7 deprecation warnings โ 0 warnings โ
- Contract size: 21.9 KB (ERC-20), 22.5 KB (ERC-721)
- Detection: ERC-721 properly detected with ERC-165 โ
๐ Additional Resourcesโ
Documentationโ
Toolsโ
- Arbiscan Sepolia
- Arbitrum Faucet
- IPFS Pinata - for NFT metadata
- NFT.Storage - free IPFS storage
Examplesโ
โ Troubleshootingโ
Common Issues & Solutions (Berdasarkan Pengalaman Real!)โ
1. Compile Error: "BYTES must be equal to Self::BYTES"โ
Error Message:
error[E0080]: evaluation of `alloy_primitives::ruint::bytes::<impl alloy_primitives::Uint<8, 1>>::to_le_bytes::<32>::{constant#1}` failed
BYTES must be equal to Self::BYTES
Penyebab: Version mismatch antara ruint dan alloy-primitives
Solusi:
# Update Cargo.toml dengan versions yang EXACT:
alloy-primitives = "=0.8.20"
alloy-sol-types = "=0.8.20"
stylus-sdk = "=0.9.0"
ruint = "=1.15.0" # CRITICAL!
# Lalu update dependencies
cargo update
Perhatikan tanda = di depan version number! Ini memaksa Cargo untuk gunakan exact version tersebut.
2. Deprecation Warnings: msg::sender() dan evm::log()โ
Warning Message:
warning: use of deprecated function `stylus_sdk::msg::sender`
warning: use of deprecated function `stylus_sdk::evm::log`
Penyebab: Sejak Stylus SDK 0.8.0+, global hostio functions sudah deprecated
Solusi Lengkap:
BEFORE (Deprecated):
use stylus_sdk::{msg, evm};
let sender = msg::sender();
evm::log(Transfer { from, to, value });
AFTER (Clean Code):
use stylus_sdk::prelude::*;
let sender = self.vm().msg_sender();
log(self.vm(), Transfer { from, to, value });
Penjelasan:
- Gunakan
self.vm().msg_sender()instead ofmsg::sender() - Gunakan
log(self.vm(), Event)instead ofevm::log(Event) - Ini pattern baru untuk better testability dan cleaner architecture
3. Cast Send Command Error: "command not found"โ
Error:
init(string,string,uint256): command not found
MyToken: command not found
Penyebab: Multi-line command tanpa backslash \
Solusi:
WRONG โ:
cast send 0x123...
"init(string,string,uint256)"
"MyToken"
"MTK"
CORRECT โ :
# Option 1: Single line
cast send 0x123... "init(string,string,uint256)" "MyToken" "MTK" --rpc-url=$RPC_URL --private-key=$PRIVATE_KEY
# Option 2: Multi-line dengan backslash
cast send 0x123... \
"init(string,string,uint256)" \
"MyToken" \
"MTK" \
--rpc-url=$RPC_URL \
--private-key=$PRIVATE_KEY
Jangan lupa include --private-key=$PRIVATE_KEY di akhir command! Sering lupa dan error "no private key found".
4. Environment Variables Not Setโ
Error:
Error: Error accessing local wallet. Did you set a private key?
Solusi:
# Set environment variables
export RPC_URL="https://sepolia-rollup.arbitrum.io/rpc"
export PRIVATE_KEY="your_key_without_0x_prefix"
# Verify mereka di-set
echo $RPC_URL
echo $PRIVATE_KEY # Should NOT be empty
# PENTING: Private key TANPA prefix 0x!
# โ
CORRECT: export PRIVATE_KEY="abc123..."
# โ WRONG: export PRIVATE_KEY="0xabc123..."
5. ERC-721 Detected as ERC-20 di Block Explorerโ
Problem: Contract ERC-721 kamu muncul sebagai "ERC-20" atau "Contract" di Arbiscan
Penyebab: Missing ERC-165 interface detection
Solusi: Tambahkan supportsInterface function dan interface IDs:
use stylus_sdk::alloy_primitives::{Address, U256, FixedBytes};
// ERC-165 Interface IDs
const ERC165_INTERFACE_ID: u32 = 0x01ffc9a7;
const ERC721_INTERFACE_ID: u32 = 0x80ac58cd;
const ERC721_METADATA_INTERFACE_ID: u32 = 0x5b5e139f;
#[public]
impl ERC721NFT {
// ... other functions ...
/// ERC-165: Check if contract supports an interface
pub fn supports_interface(&self, interface_id: FixedBytes<4>) -> bool {
let id = u32::from_be_bytes(interface_id.0);
id == ERC165_INTERFACE_ID
|| id == ERC721_INTERFACE_ID
|| id == ERC721_METADATA_INTERFACE_ID
}
}
Verify:
# Should return true
cast call <CONTRACT_ADDRESS> \
"supportsInterface(bytes4)(bool)" \
0x80ac58cd \
--rpc-url=$RPC_URL
Catatan: Block explorer mungkin perlu 10-30 menit untuk update detection. Mint minimal 1 NFT untuk trigger indexer!
6. Deploy Success tapi "Connection Refused" Errorโ
Message:
contract size: 22.5 KB
error: no error payload found in response: Transport(Custom(reqwest::Error { kind: Request, url: "http://localhost:8547/", ...
Ini BUKAN error! โ
Penjelasan:
- Contract sudah berhasil di-compile (lihat "contract size")
- Error di akhir hanya karena
cargo stylus checkcoba connect ke local node - Selama ada "contract size" output, berarti sukses!
7. Cargo.lock Needs Updateโ
Error:
error: the lock file needs to be updated but --locked was passed
Solusi:
cargo update
Ini akan regenerate Cargo.lock dengan dependency versions yang correct.
8. Function Parameter Count Mismatchโ
Error:
Error: Failed to estimate gas: execution reverted
Penyebab: Jumlah parameter salah saat call function
Contoh:
# โ WRONG - 3 parameters untuk function yang expect 2
cast send 0x123... "init(string,string,string)" "Name" "Symbol" "Extra"
# โ
CORRECT - 2 parameters
cast send 0x123... "init(string,string)" "Name" "Symbol"
Tip: Cek function signature di code kamu dulu sebelum call!
9. NFT Metadata Not Showingโ
Solusi:
- Upload images ke IPFS first (gunakan Pinata atau NFT.Storage)
- Metadata JSON harus follow standard format:
{
"name": "Token Name #1",
"description": "Description here",
"image": "ipfs://QmYourImageHash",
"attributes": [
{
"trait_type": "Background",
"value": "Blue"
}
]
} - Upload metadata JSON ke IPFS juga
- Pass IPFS URI saat mint:
ipfs://QmYourMetadataHash
10. Insufficient Funds Errorโ
Solusi:
- Get testnet ETH dari Arbitrum Sepolia Faucet
- Atau bridge dari Ethereum Sepolia ke Arbitrum Sepolia via bridge
- Check balance:
cast balance YOUR_ADDRESS --rpc-url=$RPC_URL
Pro Tips untuk Avoid Issues ๐ฏโ
- Selalu lock dependencies ke exact versions dengan
=prefix - Gunakan non-deprecated APIs (
self.vm().msg_sender(),log()) - Test compile dulu dengan
cargo stylus checksebelum deploy - Verify environment variables di-set dengan
echo $VAR_NAME - Single-line commands atau use
\for multi-line - Read error messages carefully - biasanya ada hint solution-nya
- Check Arbiscan untuk verify transactions sukses
- Mint at least 1 token/NFT untuk trigger block explorer detection
๐ Langkah Selanjutnyaโ
Persiapan untuk Mini Hackathon:
- Review semua contoh kode di modul ini
- Coba deploy token kamu sendiri
- Pikirkan ide proyek
- Cari teman satu tim di Discord
- Siap untuk membangun! ๐ ๏ธ
Sampai jumpa di Townhall 3! ๐
๐ฌ Butuh Bantuan?โ
Stuck? Ada pertanyaan?
- ๐ฌ Discord: Channel #arbitrum-stylus
- ๐ GitHub Issues: Buat issue di course repository
- ๐ฅ Study Group: Gabung sesi harian
- ๐ง Mentors: Tag @mentor di Discord
Ingat: Cara terbaik untuk belajar adalah dengan membangun. Jangan takut untuk bereksperimen dan mencoba-coba di testnet! ๐ฅ