Skip to Content
TutorialsSmart Contract Tutorial

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

FeatureTraditional JVMTOS RVM
File System AccessFull accessBlocked
Network OperationsFull accessBlockchain only
ThreadingFull supportDeterministic only
Memory ManagementGarbage collectedGas limited
ReflectionFull supportRestricted

📝 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

  1. Multi-signature Wallets: Implement shared ownership
  2. Upgradeable Contracts: Proxy pattern implementation
  3. Oracle Integration: External data feeds
  4. Cross-chain Tokens: Bridge to other networks
  5. Governance Tokens: Voting and proposals

Community Resources


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!

Last updated on