Builder 02 of 06
Your First Contract
Write, compile, and deploy a contract that stores a message. No local toolchain required to finish it.
14 min read, then one transaction
By the end of this page there will be a contract on LiteForge that you wrote, at an address that belongs to you, doing something you decided it should do. Not a copy of one. Yours.
You need a wallet on chain 4441 with a little zkLTC in it, and a browser. Nothing else — no Node, no compiler, no terminal.
A contract is a program with a bank account
Ethereum's original pitch was that a blockchain could run code, not just move money. A contract is that code once it lives on chain. Three things make it different from a program on your laptop:
- It has an address. Same shape as a wallet address, and it can hold and send funds like one.
- Its storage is permanent and public. Every variable it keeps is written into the chain's state, readable by anyone forever.
- It only runs when called. A contract has no thread of its own. It sits still until somebody sends it a transaction, executes, and stops.
That last one surprises people. There is no loop running in the background, no cron, no server. A contract is more like a vending machine than a service: it does nothing at all until you put something in.
The contract you are going to deploy
Here it is in full. It remembers a message, lets anyone replace it, and announces every change.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract MessageContract {
string public message;
address public author;
uint256 public changeCount;
event MessageChanged(address indexed author, string message, uint256 changeCount);
constructor(string memory initialMessage) {
message = initialMessage;
author = msg.sender;
changeCount = 0;
}
function setMessage(string calldata newMessage) external {
message = newMessage;
author = msg.sender;
changeCount += 1;
emit MessageChanged(msg.sender, newMessage, changeCount);
}
function read() external view returns (string memory, address) {
return (message, author);
}
}
Forty lines, and every one of them is worth understanding before you send it anywhere.
The header
SPDX-License-Identifier states the licence. The compiler warns without it.
pragma solidity ^0.8.20 says which compiler versions this source is written
for — the caret means 0.8.20 or newer, but not 0.9.
The state
string public message;
address public author;
uint256 public changeCount;
These three live in permanent storage. Marking them public does something
easy to miss: Solidity generates a getter function for each one, so the outside
world can call message() without you writing it. That is why the code has a
read() function but no getMessage() — the getter already exists.
address is a first-class type, not a string. uint256 is an unsigned integer,
and since Solidity 0.8 it reverts on overflow instead of silently wrapping.
The constructor
constructor(string memory initialMessage) {
message = initialMessage;
author = msg.sender;
changeCount = 0;
}
The constructor runs exactly once, during deployment, and then no longer exists. Whatever it writes becomes the contract's starting state.
msg.sender is whoever sent the transaction. You never have to ask a caller who
they are or trust their answer — the signature on the transaction already proved
it, and the chain hands the contract the verified result.
Changing state
function setMessage(string calldata newMessage) external {
external means it can be called from outside but not internally. calldata
means the argument is read straight from the transaction's input without being
copied into memory, which is cheaper for parameters you only read.
Then the event:
emit MessageChanged(msg.sender, newMessage, changeCount);
Events are how a contract tells the outside world something happened. They are written to the transaction log rather than to storage, which makes them far cheaper — and contracts cannot read them back. They are output only.
indexed on the author makes that field searchable, so an application can ask
the chain for every message a particular address ever wrote without downloading
all of them.
Deploying it
LitVM is EVM equivalent, so any Ethereum tool works. The fastest path with nothing installed is Remix, which compiles in your browser.
- 01
Open the contract in Remix
Use the button below this lesson to open Remix, then create a new file named MessageContract.sol and paste in the source above.
- 02
Compile it
Open the Solidity Compiler tab on the left, pick a 0.8.20 or newer compiler, and press Compile. A green tick means the source became bytecode.
- 03
Point Remix at your wallet
Open the Deploy tab and set Environment to Injected Provider. Remix now uses whatever network your wallet is on, so make sure the wallet says LitVM LiteForge, chain 4441.
- 04
Give the constructor its one argument
The field next to the Deploy button is initialMessage. On that single line a string needs its quotes, so type "Hello LitVM" with them. If you open the field into a labelled input with the chevron, type Hello LitVM there without quotes instead.
- 05
Deploy and confirm
Press Deploy and confirm in your wallet. You are paying gas in zkLTC to write your contract's code into the chain's state.
What you just created
When the transaction confirms, Remix shows a new contract address. That address did not exist before, and it was not chosen by anyone: it is derived from your address and your nonce — the count of transactions you have ever sent.
Which means something worth pausing on. Anyone can calculate, in advance, exactly what address their next contract will get. Deployment addresses are not random, they are arithmetic.
Open the address in the explorer. You will see:
- Bytecode, the compiled contract. Solidity was for you; this is what the chain actually stores and runs.
- The creation transaction, whose sender is you and whose recipient is
nobody. A deployment has no
tofield. That absence is exactly how the EVM distinguishes "create a contract" from "call one".
A normal transaction
- Has a recipient
- Runs code that already exists
- Costs gas by what it executes
- Leaves state changed
A deployment
- Has no recipient at all
- Runs the constructor once, then discards it
- Costs gas mostly by code size
- Leaves a new address in the world
Talking to it
Back in Remix, your deployed contract appears at the bottom of the Deploy tab with a button per function. Try them in this order:
message— blue button, returns instantly and costs nothing. This is the getter Solidity wrote for you.setMessage— orange, because it writes. Type a new message, press it, confirm in your wallet. This one is a real transaction.changeCount— now reads 1.read— returns the message and the author together.
Blue versus orange is Remix telling you which calls cost money. A call that only reads never leaves your machine's connection to the node. A call that writes becomes a transaction, waits for a block, and costs gas.
Find that setMessage transaction in the explorer and open its logs. Your
MessageChanged event is sitting there, with your address in the indexed field.
That log is what a frontend would subscribe to.
Letting somebody else call it
Your contract is public the moment it is deployed. There is no sharing step and no permission to grant: anyone on chain 4441 who has the address can call it, which is the whole point of a public ledger and worth feeling once.
- 01
Send them the address
That string is the entire interface. They need nothing else from you — no key, no invite, no account on anything.
- 02
They open Remix
Same compiler, same source pasted in, but instead of Deploy they use At Address: paste your contract address, press it, and the contract appears in their panel with every button.
- 03
They call setMessage
Their transaction, their gas, their address recorded as author. Nothing about it goes through you.
- 04
You read it back
Press message again on your side. Their words are there, and changeCount has gone up. One contract, two people, no server between you.
Contracts are not tokens, so there is nothing to import into a wallet here: a wallet shows balances, and this contract holds none. What it has is an address in the explorer, and that page is the shareable thing.
The same thing with Foundry
Remix is the fastest way to a first deployment, not the way you will work. When you want a real toolchain:
forge create \
--rpc-url https://liteforge.rpc.caldera.xyz/http \
--private-key $PRIVATE_KEY \
--constructor-args "Hello LitVM" \
src/MessageContract.sol:MessageContract
# read, free
cast call $ADDR "message()(string)" --rpc-url https://liteforge.rpc.caldera.xyz/http
# write, costs gas
cast send $ADDR "setMessage(string)" "Second message" \
--private-key $PRIVATE_KEY \
--rpc-url https://liteforge.rpc.caldera.xyz/http
What to remember
- 01A contract is code with an address, permanent public storage, and no thread of its own — it runs only when called.
- 02public state variables generate their own getters, which is why this contract needs no getMessage().
- 03msg.sender is proven by the transaction signature, so a contract never has to trust a caller's claim about identity.
- 04Events are write-only output: cheap, searchable when indexed, and invisible to contracts.
- 05A deployment is a transaction with no recipient, and its address is derived from your address and nonce, not chosen at random.
Primary sources
MessageContract.sol
53 lines, ready to paste · raw file
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title MessageContract
/// @notice The smallest contract that is still interesting: it remembers a
/// message, lets anyone change it, and announces every change.
///
/// This is the contract you deploy in "Your First Contract" at
/// https://litvm-academy.pro/build/your-first-contract
contract MessageContract {
/// @notice The current message.
/// @dev Marking it `public` makes Solidity generate a `message()` getter
/// for free, which is what the outside world will call to read it.
string public message;
/// @notice Who wrote the current message.
address public author;
/// @notice How many times the message has been changed.
uint256 public changeCount;
/// @notice Emitted on every change.
/// @dev Events are the cheap way for a contract to tell the outside world
/// that something happened. `indexed` lets anyone filter the log by
/// author without reading every event ever emitted.
event MessageChanged(address indexed author, string message, uint256 changeCount);
/// @dev Runs once, at deployment, and is never callable again. Whatever it
/// writes becomes the contract's starting state.
constructor(string memory initialMessage) {
message = initialMessage;
author = msg.sender;
changeCount = 0;
}
/// @notice Replace the message.
/// @dev `msg.sender` is whoever sent the transaction. The chain proves it,
/// so a contract never has to ask who is calling.
function setMessage(string calldata newMessage) external {
message = newMessage;
author = msg.sender;
changeCount += 1;
emit MessageChanged(msg.sender, newMessage, changeCount);
}
/// @notice Read the message and who wrote it in one call.
/// @dev `view` promises this changes nothing, so calling it costs no gas
/// when you are only reading.
function read() external view returns (string memory, address) {
return (message, author);
}
}
Your first contract
Paste the hash of the transaction that deployed your contract. The check reads the transaction straight from LiteForge, so it has to be one your own wallet sent.