Most times, when people start learning how to write smart contracts, the first thing they hear about is Solidity and Ethereum. That was the first thing I heard about too. It's what most tutorials focus on, and for good reason. Solidity made it possible to write programs that live on a blockchain, and Ethereum became the place where many people got started.
But Solidity isn’t the only smart contract language out there. And Ethereum isn’t the only blockchain that supports decentralized applications.
There’s also TON, short for The Open Network. It was created by Telegram, but it's now a public, community-driven chain. It’s fast, lightweight, and handles things a bit differently from what you might be used to on Ethereum. That includes how smart contracts are written. When I started exploring the TON documentation, I came across four different languages for writing smart contracts: Tact, Tolk, FunC, and Fift. I won’t go deep into all four here.
This guide focuses on the Tact language, and we’ll see how to use it to build a basic voting contract that lets users cast votes and check results on-chain.
The TON ecosystem actually supports multiple languages, each serving different use cases, levels of abstraction, and developer experience. Here’s a quick overview of the of each of them:
Tact provides and a quicker path for building and deploying contracts on the TON blockchain.
Before we start writing code, it’s important to understand how Tact smart contracts are structured. A typical Tact contract includes a few core components:
contract
block – This is where you define the name of your contract and declare any state variables.
init
block – It initializes your contract’s state variables and sets the contract’s starting conditions. This block runs once at the time of deployment.
receive
blocks – These are like event listeners. They handle incoming messages and define how your contract reacts to them.
Getter functions (get fun
) – These are optional read-only functions that allow users or other contracts to query the contract’s state without changing it.
Tact uses message-based communication, which is how all interactions on TON work. Each contract receives a message and processes it in its own receive
block. This message-based structure helps organize your contract logic in a modular, maintainable way.
Let’s now apply this in a real example by building a simple voting contract.
In this section, we'll walk through how to implement a basic voting system using Tact. This voting contract will allows users to vote for predefined candidates and tracks the total number of votes each candidate receives.
We’ll be doing everything inside the TON Web IDE, which is an in-browser tool where you can write, build, and test your contracts without installing anything locally.
VotingContract
.
After creating your project, open the main.tact
file. You’ll see a boilerplate setup:
// Import the Deployable trait so the contract can be deployed easily
import "@stdlib/deploy";
contract BlankContract with Deployable {
init() {
}
}
import "@stdlib/deploy";
is required for deployment to work and should not be removed from the code.BlankContract
is the placeholder name.init()
block runs only once when the contract is deployed and is used to initialize state variables.
Now, let’s map out our own code.
First, we’ll define the message structure for voting:
// Import the Deployable trait so the contract can be deployed easily
import "@stdlib/deploy";
// Define a message structure for voting
message Vote {
candidate: Int as uint32; // 1 = Alice, 2 = Bob
}
This is the Vote message. When someone wants to vote, they’ll send a message to the contract that includes a number:
Tact uses this structure to process the incoming vote and decide which candidate gets the point.
Next, we’ll set up our contract and add two state variables to keep track of each candidate’s votes:
...
contract VotingContract with Deployable {
// State variables to track votes
votesAlice: Int as uint32;
votesBob: Int as uint32;
Inside the contract, we defined two variables:
votesAlice
: stores the number of votes Alice receives.votesBob
: stores the number of votes Bob receives.
We’ll now initialize those vote counts to zero inside the init
block to set the starting state of the contract when it's first deployed.
init() {
self.votesAlice = 0;
self.votesBob = 0;
}
The init
block runs only once, right when the contract is deployed and it sets both vote counts to zero.
Now comes the logic. When a vote is sent, we want the contract to check who the vote is for and increase the correct vote count.
// Handle vote messages
receive(msg: Vote) {
if (msg.candidate == 1) {
self.votesAlice += 1;
} else if (msg.candidate == 2) {
self.votesBob += 1;
}
}
So when a vote is received:
msg.candidate
is 1, we add +1 to votesAlice
msg.candidate
is 2, we add +1 to votesBob
Finally, we’ll create getter functions to let anyone query the vote count for each candidate without changing the contract state.
// Getter for Alice's votes
get fun getVotesForAlice(): Int {
return self.votesAlice;
}
// Getter for Bob's votes
get fun getVotesForBob(): Int {
return self.votesBob;
}
}
These two getter functions let us check the number of votes each candidate has received without modifying anything in the contract. It’s a read-only operation.
Below is the full voting contract code:
import "@stdlib/deploy";
// Define a message structure for voting
message Vote {
candidate: Int as uint32; // 1 = Alice, 2 = Bob
}
contract VotingContract with Deployable {
// State variables to track votes
votesAlice: Int as uint32;
votesBob: Int as uint32;
init() {
self.votesAlice = 0;
self.votesBob = 0;
}
// Handle vote messages
receive(msg: Vote) {
if (msg.candidate == 1) {
self.votesAlice += 1;
} else if (msg.candidate == 2) {
self.votesBob += 1;
}
}
// Getter for Alice's votes
get fun getVotesForAlice(): Int {
return self.votesAlice;
}
// Getter for Bob's votes
get fun getVotesForBob(): Int {
return self.votesBob;
}
}
Once deployed, scroll down and you’ll see two sections:
getVotesForAlice
, getVotesForBob
Vote
To cast a vote: In the Vote section, enter 1
in the candidate
input field and click Send. You’ve just voted for Alice! You can repeat this to cast more votes.
To check the vote count: Click Call under getVotesForAlice
and check the logs panel to see the vote count
2
in the candidate
field, then check getVotesForBob
In my test run, I voted for Alice 9 times and Bob 6 times, and the getter functions showed exactly that.
🙌 Congrats if you read all the way through!
Now that you’ve seen how a simple voting contract works in Tact, you’ve taken your first step into smart contract development on TON. This contract might be basic, but the structure and concepts apply to more complex logic too.
If you want to keep experimenting, try extending this contract or exploring other prebuilt templates from https://tact-by-example.org/all. The TON Web IDE also makes it easy to try out different use cases and it comes with templates as well to help you build and learn faster.
So go ahead, tweak, test, build something better.