Skip to content
LitVM AcademyStart
Builder track

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 to field. 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:

  1. message — blue button, returns instantly and costs nothing. This is the getter Solidity wrote for you.
  2. setMessage — orange, because it writes. Type a new message, press it, confirm in your wallet. This one is a real transaction.
  3. changeCount — now reads 1.
  4. 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.

  1. 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.

  2. 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.

  3. 03

    They call setMessage

    Their transaction, their gas, their address recorded as author. Nothing about it goes through you.

  4. 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);
    }
}
Open RemixCreate a new file named MessageContract.sol, paste in the contract above, then compile it with Solidity 0.8.20 or newer and deploy it from Remix.Open

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.