Smart Contract Development Tutorial
Learn to build powerful smart contracts using familiar Java syntax on TOS Network’s Rust Virtual Machine (RVM). This tutorial will guide you through creating a complete token contract from scratch.
🎯 Learning Objectives
By the end of this tutorial, you will:
- Understand RVM architecture and Java compatibility
- Build a complete ERC-20 compatible token contract
- Implement advanced features like staking and governance
- Deploy contracts to testnet and mainnet
- Test contract functionality and security
⏱️ Estimated Time: 2 hours
🛠️ Prerequisites
Required Knowledge
- Basic Java programming (classes, methods, inheritance)
- Understanding of blockchain concepts (transactions, blocks, addresses)
- Familiarity with token standards (helpful but not required)
Development Setup
# Install TOS SDK
npm install -g @tos-network/sdk
# Verify installation
tos-sdk --version
# Create project directory
mkdir tos-token-tutorial
cd tos-token-tutorial
# Initialize project
tos-sdk init --template smart-contract
Environment Configuration
# Set up testnet configuration
tos-sdk config set network testnet
tos-sdk config set api-key YOUR_TESTNET_API_KEY
# Get testnet tokens from faucet
tos-sdk faucet request --address YOUR_WALLET_ADDRESS
📝 Step 1: Understanding RVM
RVM Architecture Overview
The Rust Virtual Machine (RVM) enables Java 8 code execution on TOS Network:
// This is valid Java 8 code that runs on RVM
public class HelloTOS {
private String message;
public HelloTOS(String initialMessage) {
this.message = initialMessage;
}
public String getMessage() {
return message;
}
public void setMessage(String newMessage) {
this.message = newMessage;
}
}
Key RVM Features
- Full Java 8 Compatibility: Standard Java syntax and libraries
- Blockchain Integration: Special classes for blockchain operations
- Gas Metering: Automatic resource management
- Security Sandbox: Restricted system access
- Deterministic Execution: Identical results across all nodes
RVM vs Traditional JVM
Feature | Traditional JVM | TOS RVM |
---|---|---|
File System Access | Full access | Blocked |
Network Operations | Full access | Blockchain only |
Threading | Full support | Deterministic only |
Memory Management | Garbage collected | Gas limited |
Reflection | Full support | Restricted |
📝 Step 2: Basic Contract Structure
Contract Template
Create src/main/java/MyToken.java
:
import network.tos.contracts.Contract;
import network.tos.contracts.Address;
import network.tos.contracts.BigInteger;
import network.tos.contracts.Events;
/**
* MyToken - A complete ERC-20 compatible token contract
*/
public class MyToken extends Contract {
// Token metadata
private final String name = "MyToken";
private final String symbol = "MTK";
private final int decimals = 18;
// Token state
private BigInteger totalSupply;
private Map<Address, BigInteger> balances;
private Map<Address, Map<Address, BigInteger>> allowances;
// Events
public static class Transfer extends Events.Event {
public final Address from;
public final Address to;
public final BigInteger value;
public Transfer(Address from, Address to, BigInteger value) {
this.from = from;
this.to = to;
this.value = value;
}
}
public static class Approval extends Events.Event {
public final Address owner;
public final Address spender;
public final BigInteger value;
public Approval(Address owner, Address spender, BigInteger value) {
this.owner = owner;
this.spender = spender;
this.value = value;
}
}
/**
* Constructor - Initialize token with initial supply
*/
public MyToken(BigInteger initialSupply) {
this.totalSupply = initialSupply.multiply(BigInteger.valueOf(10).pow(decimals));
this.balances = new HashMap<>();
this.allowances = new HashMap<>();
// Give all tokens to contract creator
Address creator = getDeployer();
this.balances.put(creator, this.totalSupply);
// Emit initial transfer event
emit(new Transfer(Address.ZERO, creator, this.totalSupply));
}
// Getter methods
public String name() { return name; }
public String symbol() { return symbol; }
public int decimals() { return decimals; }
public BigInteger totalSupply() { return totalSupply; }
public BigInteger balanceOf(Address account) {
return balances.getOrDefault(account, BigInteger.ZERO);
}
public BigInteger allowance(Address owner, Address spender) {
return allowances.getOrDefault(owner, new HashMap<>())
.getOrDefault(spender, BigInteger.ZERO);
}
}
Understanding Contract Base Class
import network.tos.contracts.Contract;
public abstract class Contract {
// Blockchain context methods
protected Address getCaller(); // Transaction sender
protected Address getDeployer(); // Contract deployer
protected BigInteger getValue(); // TOS sent with transaction
protected long getBlockHeight(); // Current block height
protected long getTimestamp(); // Block timestamp
// Gas and execution control
protected void requireGas(long amount);
protected void refundGas(long amount);
// Event emission
protected void emit(Events.Event event);
// Assertions and validation
protected void require(boolean condition, String message);
protected void revert(String message);
}
📝 Step 3: Implementing Core Functions
Transfer Functionality
/**
* Transfer tokens from caller to recipient
*/
public boolean transfer(Address to, BigInteger amount) {
Address from = getCaller();
return _transfer(from, to, amount);
}
/**
* Transfer tokens from one address to another (with allowance)
*/
public boolean transferFrom(Address from, Address to, BigInteger amount) {
Address spender = getCaller();
// Check allowance
BigInteger currentAllowance = allowance(from, spender);
require(currentAllowance.compareTo(amount) >= 0,
"Transfer amount exceeds allowance");
// Perform transfer
boolean success = _transfer(from, to, amount);
if (success) {
// Reduce allowance
_approve(from, spender, currentAllowance.subtract(amount));
}
return success;
}
/**
* Internal transfer implementation
*/
private boolean _transfer(Address from, Address to, BigInteger amount) {
require(from != null && to != null, "Transfer to/from zero address");
require(amount.compareTo(BigInteger.ZERO) > 0, "Transfer amount must be positive");
BigInteger fromBalance = balanceOf(from);
require(fromBalance.compareTo(amount) >= 0, "Transfer amount exceeds balance");
// Update balances
balances.put(from, fromBalance.subtract(amount));
balances.put(to, balanceOf(to).add(amount));
// Emit transfer event
emit(new Transfer(from, to, amount));
return true;
}
Approval System
/**
* Approve spender to transfer tokens on behalf of caller
*/
public boolean approve(Address spender, BigInteger amount) {
Address owner = getCaller();
return _approve(owner, spender, amount);
}
/**
* Increase allowance for spender
*/
public boolean increaseAllowance(Address spender, BigInteger addedValue) {
Address owner = getCaller();
BigInteger currentAllowance = allowance(owner, spender);
return _approve(owner, spender, currentAllowance.add(addedValue));
}
/**
* Decrease allowance for spender
*/
public boolean decreaseAllowance(Address spender, BigInteger subtractedValue) {
Address owner = getCaller();
BigInteger currentAllowance = allowance(owner, spender);
require(currentAllowance.compareTo(subtractedValue) >= 0,
"Decreased allowance below zero");
return _approve(owner, spender, currentAllowance.subtract(subtractedValue));
}
/**
* Internal approval implementation
*/
private boolean _approve(Address owner, Address spender, BigInteger amount) {
require(owner != null && spender != null, "Approve to/from zero address");
allowances.computeIfAbsent(owner, k -> new HashMap<>()).put(spender, amount);
emit(new Approval(owner, spender, amount));
return true;
}
📝 Step 4: Advanced Features
Minting and Burning
// Access control
private Address owner;
private Map<Address, Boolean> minters;
// Constructor addition
public MyToken(BigInteger initialSupply) {
// ... existing constructor code ...
this.owner = getDeployer();
this.minters = new HashMap<>();
this.minters.put(owner, true);
}
/**
* Mint new tokens (only authorized minters)
*/
public boolean mint(Address to, BigInteger amount) {
Address caller = getCaller();
require(minters.getOrDefault(caller, false), "Caller is not a minter");
require(to != null, "Cannot mint to zero address");
require(amount.compareTo(BigInteger.ZERO) > 0, "Mint amount must be positive");
// Update total supply and balance
totalSupply = totalSupply.add(amount);
balances.put(to, balanceOf(to).add(amount));
emit(new Transfer(Address.ZERO, to, amount));
return true;
}
/**
* Burn tokens from caller's balance
*/
public boolean burn(BigInteger amount) {
Address caller = getCaller();
BigInteger callerBalance = balanceOf(caller);
require(callerBalance.compareTo(amount) >= 0, "Burn amount exceeds balance");
require(amount.compareTo(BigInteger.ZERO) > 0, "Burn amount must be positive");
// Update total supply and balance
totalSupply = totalSupply.subtract(amount);
balances.put(caller, callerBalance.subtract(amount));
emit(new Transfer(caller, Address.ZERO, amount));
return true;
}
/**
* Add minter (only owner)
*/
public boolean addMinter(Address minter) {
require(getCaller().equals(owner), "Only owner can add minters");
minters.put(minter, true);
return true;
}
Staking System
// Staking state
private Map<Address, StakeInfo> stakes;
private BigInteger totalStaked;
private final BigInteger STAKING_REWARD_RATE = BigInteger.valueOf(5); // 5% APR
public static class StakeInfo {
public BigInteger amount;
public long startTime;
public BigInteger rewards;
public StakeInfo(BigInteger amount, long startTime) {
this.amount = amount;
this.startTime = startTime;
this.rewards = BigInteger.ZERO;
}
}
/**
* Stake tokens to earn rewards
*/
public boolean stake(BigInteger amount) {
Address staker = getCaller();
require(amount.compareTo(BigInteger.ZERO) > 0, "Stake amount must be positive");
require(balanceOf(staker).compareTo(amount) >= 0, "Insufficient balance");
// Update rewards before changing stake
updateRewards(staker);
// Transfer tokens to staking
balances.put(staker, balanceOf(staker).subtract(amount));
totalStaked = totalStaked.add(amount);
// Update stake info
StakeInfo currentStake = stakes.getOrDefault(staker,
new StakeInfo(BigInteger.ZERO, getTimestamp()));
currentStake.amount = currentStake.amount.add(amount);
stakes.put(staker, currentStake);
return true;
}
/**
* Unstake tokens and claim rewards
*/
public boolean unstake(BigInteger amount) {
Address staker = getCaller();
StakeInfo stakeInfo = stakes.get(staker);
require(stakeInfo != null, "No stake found");
require(stakeInfo.amount.compareTo(amount) >= 0, "Insufficient staked amount");
// Update rewards
updateRewards(staker);
// Return staked tokens
balances.put(staker, balanceOf(staker).add(amount));
totalStaked = totalStaked.subtract(amount);
// Update stake info
stakeInfo.amount = stakeInfo.amount.subtract(amount);
if (stakeInfo.amount.equals(BigInteger.ZERO)) {
stakes.remove(staker);
}
return true;
}
/**
* Update staking rewards for address
*/
private void updateRewards(Address staker) {
StakeInfo stakeInfo = stakes.get(staker);
if (stakeInfo == null || stakeInfo.amount.equals(BigInteger.ZERO)) {
return;
}
long currentTime = getTimestamp();
long stakingDuration = currentTime - stakeInfo.startTime;
// Calculate rewards: (amount * rate * time) / (365 * 24 * 60 * 60)
BigInteger timeReward = stakeInfo.amount
.multiply(STAKING_REWARD_RATE)
.multiply(BigInteger.valueOf(stakingDuration))
.divide(BigInteger.valueOf(365 * 24 * 60 * 60 * 100)); // 100 for percentage
stakeInfo.rewards = stakeInfo.rewards.add(timeReward);
stakeInfo.startTime = currentTime;
}
/**
* Claim staking rewards
*/
public boolean claimRewards() {
Address claimer = getCaller();
updateRewards(claimer);
StakeInfo stakeInfo = stakes.get(claimer);
require(stakeInfo != null && stakeInfo.rewards.compareTo(BigInteger.ZERO) > 0,
"No rewards to claim");
BigInteger rewards = stakeInfo.rewards;
stakeInfo.rewards = BigInteger.ZERO;
// Mint rewards (or transfer from reward pool)
balances.put(claimer, balanceOf(claimer).add(rewards));
totalSupply = totalSupply.add(rewards);
emit(new Transfer(Address.ZERO, claimer, rewards));
return true;
}
📝 Step 5: Testing Your Contract
Unit Tests
Create src/test/java/MyTokenTest.java
:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;
import network.tos.testing.ContractTestRunner;
import network.tos.contracts.Address;
import network.tos.contracts.BigInteger;
public class MyTokenTest {
private ContractTestRunner runner;
private MyToken token;
private Address deployer;
private Address user1;
private Address user2;
@BeforeEach
void setUp() {
runner = new ContractTestRunner();
deployer = runner.createAddress("deployer");
user1 = runner.createAddress("user1");
user2 = runner.createAddress("user2");
// Deploy contract
token = runner.deploy(MyToken.class, deployer,
BigInteger.valueOf(1000000)); // 1M tokens
}
@Test
void testInitialSupply() {
assertEquals(BigInteger.valueOf(1000000).multiply(BigInteger.valueOf(10).pow(18)),
token.totalSupply());
assertEquals(token.totalSupply(), token.balanceOf(deployer));
assertEquals(BigInteger.ZERO, token.balanceOf(user1));
}
@Test
void testTransfer() {
BigInteger transferAmount = BigInteger.valueOf(1000).multiply(BigInteger.valueOf(10).pow(18));
// Transfer from deployer to user1
runner.setCaller(deployer);
assertTrue(token.transfer(user1, transferAmount));
// Check balances
assertEquals(transferAmount, token.balanceOf(user1));
assertEquals(token.totalSupply().subtract(transferAmount), token.balanceOf(deployer));
}
@Test
void testApproveAndTransferFrom() {
BigInteger approveAmount = BigInteger.valueOf(500).multiply(BigInteger.valueOf(10).pow(18));
BigInteger transferAmount = BigInteger.valueOf(300).multiply(BigInteger.valueOf(10).pow(18));
// Deployer approves user1 to spend tokens
runner.setCaller(deployer);
assertTrue(token.approve(user1, approveAmount));
assertEquals(approveAmount, token.allowance(deployer, user1));
// User1 transfers from deployer to user2
runner.setCaller(user1);
assertTrue(token.transferFrom(deployer, user2, transferAmount));
// Check balances and allowance
assertEquals(transferAmount, token.balanceOf(user2));
assertEquals(approveAmount.subtract(transferAmount), token.allowance(deployer, user1));
}
@Test
void testStaking() {
BigInteger stakeAmount = BigInteger.valueOf(1000).multiply(BigInteger.valueOf(10).pow(18));
// Transfer some tokens to user1 first
runner.setCaller(deployer);
token.transfer(user1, stakeAmount.multiply(BigInteger.valueOf(2)));
// User1 stakes tokens
runner.setCaller(user1);
BigInteger balanceBefore = token.balanceOf(user1);
assertTrue(token.stake(stakeAmount));
// Check balance decreased
assertEquals(balanceBefore.subtract(stakeAmount), token.balanceOf(user1));
// Simulate time passage and check rewards
runner.increaseTime(365 * 24 * 60 * 60); // 1 year
assertTrue(token.claimRewards());
// Should have earned approximately 5% APR
assertTrue(token.balanceOf(user1).compareTo(balanceBefore.subtract(stakeAmount)) > 0);
}
@Test
void testFailures() {
BigInteger transferAmount = BigInteger.valueOf(1000).multiply(BigInteger.valueOf(10).pow(18));
// Transfer more than balance should fail
runner.setCaller(user1);
assertThrows(Exception.class, () -> {
token.transfer(user2, transferAmount);
});
// Transfer to zero address should fail
runner.setCaller(deployer);
assertThrows(Exception.class, () -> {
token.transfer(null, transferAmount);
});
// Non-minter cannot mint
runner.setCaller(user1);
assertThrows(Exception.class, () -> {
token.mint(user1, transferAmount);
});
}
}
Running Tests
# Run all tests
tos-sdk test
# Run specific test
tos-sdk test MyTokenTest
# Run with coverage
tos-sdk test --coverage
# Generate test report
tos-sdk test --report
📝 Step 6: Deployment
Testnet Deployment
# Compile contract
tos-sdk compile
# Deploy to testnet
tos-sdk deploy --network testnet --contract MyToken --args 1000000
# Example output:
# Contract deployed successfully!
# Address: tos1contract_abc123...
# Transaction: tx_def456...
# Gas used: 234567
Deployment Script
Create scripts/deploy.js
:
const { TOSNetwork, ContractFactory } = require('@tos-network/sdk');
async function deployToken() {
// Connect to network
const tos = new TOSNetwork({
network: 'testnet',
apiKey: process.env.TOS_API_KEY
});
// Load wallet
const wallet = await tos.wallet.fromPrivateKey(process.env.PRIVATE_KEY);
// Create contract factory
const factory = new ContractFactory(
'MyToken',
'./build/MyToken.class',
wallet
);
// Deploy contract
console.log('Deploying MyToken...');
const contract = await factory.deploy(1000000); // 1M initial supply
console.log(`Contract deployed at: ${contract.address}`);
console.log(`Transaction hash: ${contract.deploymentTransaction.hash}`);
// Verify deployment
const totalSupply = await contract.totalSupply();
console.log(`Total supply: ${totalSupply}`);
return contract;
}
deployToken().catch(console.error);
Verification
# Verify contract on block explorer
tos-sdk verify --address tos1contract_abc123... --source ./src/main/java/MyToken.java
# Interact with deployed contract
tos-sdk console --contract tos1contract_abc123...
# In console:
> token.totalSupply()
> token.balanceOf("tos1your_address...")
> token.transfer("tos1recipient...", "1000000000000000000") // 1 token
💡 Best Practices
Security Considerations
// Use SafeMath for arithmetic operations
import network.tos.contracts.SafeMath;
public boolean transfer(Address to, BigInteger amount) {
BigInteger balance = balanceOf(getCaller());
// Use SafeMath to prevent overflow/underflow
require(SafeMath.gte(balance, amount), "Insufficient balance");
balances.put(getCaller(), SafeMath.sub(balance, amount));
balances.put(to, SafeMath.add(balanceOf(to), amount));
return true;
}
// Input validation
private void validateAddress(Address addr) {
require(addr != null, "Address cannot be null");
require(!addr.equals(Address.ZERO), "Address cannot be zero");
}
// Access control
modifier onlyOwner() {
require(getCaller().equals(owner), "Only owner can call this function");
}
Gas Optimization
// Cache repeated calculations
public boolean batchTransfer(Address[] recipients, BigInteger[] amounts) {
require(recipients.length == amounts.length, "Array length mismatch");
Address sender = getCaller();
BigInteger senderBalance = balanceOf(sender);
BigInteger totalAmount = BigInteger.ZERO;
// Calculate total first
for (BigInteger amount : amounts) {
totalAmount = totalAmount.add(amount);
}
require(senderBalance.compareTo(totalAmount) >= 0, "Insufficient balance");
// Perform transfers
balances.put(sender, senderBalance.subtract(totalAmount));
for (int i = 0; i < recipients.length; i++) {
balances.put(recipients[i], balanceOf(recipients[i]).add(amounts[i]));
emit(new Transfer(sender, recipients[i], amounts[i]));
}
return true;
}
Event Design
// Detailed events for better tracking
public static class StakeEvent extends Events.Event {
public final Address staker;
public final BigInteger amount;
public final BigInteger totalStaked;
public final long timestamp;
public StakeEvent(Address staker, BigInteger amount, BigInteger totalStaked, long timestamp) {
this.staker = staker;
this.amount = amount;
this.totalStaked = totalStaked;
this.timestamp = timestamp;
}
}
🔧 Troubleshooting
Common Issues
Contract Compilation Errors:
# Check Java version
java -version # Should be 8 or higher
# Verify imports
# Make sure all TOS imports are correct
# Check syntax
# RVM supports Java 8 syntax only
Deployment Failures:
# Check gas limit
tos-sdk deploy --gas-limit 1000000
# Verify network connection
tos-sdk network status
# Check wallet balance
tos-sdk wallet balance
Test Failures:
# Run tests with verbose output
tos-sdk test --verbose
# Check test environment
tos-sdk test --debug
# Verify mock data
tos-sdk test --trace
Debugging Tools
// Add logging to contracts (testnet only)
import network.tos.contracts.Debug;
public boolean transfer(Address to, BigInteger amount) {
Debug.log("Transfer called", getCaller(), to, amount);
// ... transfer logic ...
Debug.log("Transfer completed", success);
return success;
}
🚀 Next Steps
Advanced Topics to Explore
- Multi-signature Wallets: Implement shared ownership
- Upgradeable Contracts: Proxy pattern implementation
- Oracle Integration: External data feeds
- Cross-chain Tokens: Bridge to other networks
- Governance Tokens: Voting and proposals
Recommended Tutorials
- DeFi Integration - Add liquidity and trading
- Privacy Implementation - Confidential tokens
- Performance Optimization - Scale your contracts
- Security Best Practices - Secure development
Community Resources
- Code Examples: github.com/tos-network/contract-examples
- Developer Chat: discord.gg/tos-dev
- Stack Overflow: Tag your questions with
tos-network
- Office Hours: Weekly developer Q&A sessions
Congratulations! You’ve successfully built a complete token contract with advanced features. Your contract is now ready for production use on TOS Network.
“Don’t Trust, Verify it” - Remember to thoroughly test your contracts and consider professional audits before mainnet deployment with significant value!