Building with Solidity

Smart Contracts on VeChain

This lesson introduces smart contract fundamentals on VeChain - covering contract structure, Solidity syntax, and the core building blocks of variables, functions, and events that store data and automate blockchain logic.

Smart Contracts Basics

Smart contracts are more than just code on the blockchain; they’re self-executing logic that runs exactly as written. On VeChain, these contracts automate everything from token transfers to sustainability rewards.

Contract Anatomy: The Basics

For this lesson, we will be deconstructing this contract

At the top of the contract, we declare metadata and the Solidity version:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
  • SPDX-License-Identifier: Lets tools like block explorers identify the license.

  • pragma: Specifies the Solidity compiler version your contract is written for.

Then we define the contract:

contract Learn2Earn {}

In Solidity, contract Learn2Earn { ... } works similarly to defining a class in other programming languages (like class Learn2Earn { ... } in Java or Python). You're declaring a container that will hold:

  • State variables (e.g. Student)

  • Functions (e.g. addStudent)

  • Events (e.g. CertificateIssued)

  • Modifiers

  • Any business logic you want to run on-chain

Once deployed to VeChainThor, this contract becomes a standalone program on the blockchain. It has its own address, stores data (state), and can receive and send transactions.

Variables, Functions, and Events in Solidity

Solidity gives you the tools to build logic and state into your smart contracts. In this lesson, we’ll focus on three core building blocks:

  1. Variables – used to store data onchain

  2. Events – used to communicate actions off-chain (e.g., to your frontend)

  3. Functions – used to read from or write to that data

State Variables

State variables are stored permanently on the blockchain. They keep track of important contract data, like user info, balances, or settings, and their values persist between function calls.

Changing a state variable updates the contract’s storage, which costs gas and creates a permanent record. You’ll use them anytime your contract needs memory.

Common Solidity Data Types

  • uint256 – Unsigned integer (only positive numbers). Used for balances, counters, etc.
    uint256 totalPoints;

  • bool – Boolean (true or false). Used for flags, toggles, access checks.
    bool isActive;

  • address – Blockchain address (wallet or contract).
    address public owner;

  • string – Text data. Used for names, descriptions, etc.
    string public username;

  • mapping – Key/value store for storing relationships. Often maps address => data.
    mapping(address => uint256) public balances; 

Example:

mapping(address => Student) public students;
students[msg.sender] = Student({ wallet: msg.sender, name: _name });
string memory name = students[msg.sender].name;
  • struct – Custom data type that groups related values. Great for modeling things like user profiles or transactions.

Example:

struct Student {
  address wallet;
  string name;
}

Events: Real-Time Onchain Logs

Events in Solidity are essentially console logs that are stored onchain forever. They’re for external tools and interfaces to track contract activity. These logs aren’t stored in contract storage (so they’re cheaper), but they’re accessible to off-chain tools like:

  • dApp frontends

  • Indexers

  • Block explorers (e.g. VeChain Explorer, VeChain Stats)

  • Event-driven backends or automation scripts

Here's a typical event definition:

event CertificateIssued(string institute, bytes32 certificateHash, address student);

This event is structured to log:

  • A string for the institute

  • An indexed bytes32 certificateHash

  • The address of the user/student

Indexed parameters make it easier for off-chain services to filter and query logs based on those fields. 

Emitting Events

Events are triggered with the emit keyword:

emit CertificateIssued(institute, student.certificate, student.wallet);

This tells the blockchain to record a new log entry. While it doesn’t change state, it leaves a public trace of what happened and when.

Frontend Integration Example

On VeChainThor, you can listen for smart contract events using the official @vechain/sdk-network package. This allows your frontend or backend to react in real time when specific events are emitted on-chain.

🔁 Example Use Case

Let’s say your contract emits this event:

event CertificateIssued(string institute, bytes32 certificateHash, address student);

You want your frontend to react every time a new certificate is issued. To enable this, use VeChain’s WebSocket-based event subscription like this:

Code Example:

import { subscriptions } from '@vechain/sdk-network';
import WebSocket from 'ws';
const wsUrl = subscriptions.getEventSubscriptionUrl(
  'https://mainnet.vechain.org', // or testnet URL
  'event CertificateIssued(string institute, bytes32 certificateHash, address student)',
  [],
  { address: '0xYourContractAddressHere' }
);
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
  console.log('Connected to', wsUrl);
};
ws.onmessage = (message) => {
  const CertificateIssued = JSON.parse(message.data);
  console.log('New certificate issued:', CertificateIssued);
};

This connection listens for CertificateIssued events from your deployed contract and streams them live to your app.

Functions

Functions are how your smart contract interacts with the world. In Solidity, they define what your contract can do, from returning a user’s data to updating the blockchain with new information. In this part of the course, we’ll look at several real examples from the Focus contract, explaining what they do, how they’re structured, and why they matter.

View vs. State-Changing Functions

Functions in Solidity are either read-only (view) or state-changing.

  • View functions read from the blockchain but don’t modify it. They don’t cost gas when called externally (like from your frontend).

  • State-changing functions write to storage. These do cost gas, since they permanently alter the blockchain state.

For example:

  • addStudent() and issueCertificate() are state-changing functions; they update users’ statues simply return stored values.

  • isGraduated() is a view function, returns the user (student) status.

Writing to storage is the most gas-expensive operation in Solidity, so efficient design matters. Even on VeChain, where your dApp might cover fees for users (thanks to fee delegation), optimizing logic keeps your app responsive and scalable.

Gas Implications

When designing functions, keep in mind that:

  • Reading from storage (view functions) is free when called from outside the chain.

  • Writing to storage is costly — every storage update increases gas usage.

  • Unbounded loops or unnecessary state changes can make contracts expensive to run.

  • Access modifiers like onlyOwner don’t affect gas much but are crucial for security.

As you continue building, you’ll start seeing where smart logic can reduce cost, or where bad design can make your app slow or expensive to use.

Here’s an example of a simple view function:

function isGraduated(address studentAddress) public view returns (bool) {
     return students[studentAddress].graduated;
}

This returns a user's stored profile data without modifying anything. Since it’s marked view, it costs no gas to call.

Here’s a function that updates state:

function addStudent(string memory _name, string memory _familyName) public payable {
        require(msg.value == 1 ether, "You must pay 1 VET to register.");
        // Make sure the student isn't already registered
        require(!students[msg.sender].registered, "You are already registered.");
        // Create a new student record
        students[msg.sender] = Student({
            wallet: msg.sender,
            name: _name,
            familyName: _familyName,
            registered: true,
            graduated: false,
            certificate: 0
        });
    }

Visibility

In Solidity, every function has a visibility level that defines who can call it:

  • public: Anyone can call it, including external users and other contracts

  • external: Only callable from outside the contract (e.g., by a user or dApp)

  • internal: Only functions inside the same contract (or inherited ones) can call it

  • private: Only functions inside this specific contract can access it

In our examples:

  • addStudent and issueCertificate are marked public, making them accessible to users and dApps

  • _checkStudent is marked private

Use visibility to clearly separate your internal logic from what users are allowed to call. This helps secure your contract and improves readability.