Skip to main content
T TON Adoption
Basics DEV · 2026

ACTON: First Smart Contract on TON — Hands-On Tutorial (2026)

Install Acton, write your first Tolk counter contract, run tests, mutation testing, fuzzing, deploy to testnet — step-by-step hands-on for TON developers.

Author
TON Adoption Team · research desk
Published
7 min read

TL;DR. Acton v1.0 from TON Foundation (released 11 May 2026) consolidates the TON stack into one CLI: acton new, acton build, acton test, acton deploy. This tutorial walks through it step-by-step: install → counter project → Tolk code → unit tests → mutation testing → testnet deploy → call via TonAPI. From an empty machine to a working testnet contract — 2 hours. This is a practical companion to our overview ACTON guide.

What we’re building

The simplest possible contract: a counter.

  • Stores counter (uint32) in the data cell.
  • Accepts op::increment (op=0x7e8764ef) and op::reset (op=0xa5a4e3d2).
  • Get-method getCounter() returns the current value.

“Hello world” for TON, like useState in React or tasks in Express. Master this and you have the full workflow for any larger contract.

Step 1: Install Acton

macOS / Linux

curl -fsSL https://acton.ton.org/install.sh | sh

The script drops the binary at ~/.local/bin/acton. Add to PATH:

echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc  # or ~/.bashrc
source ~/.zshrc
acton --version  # should print "acton 1.0.0"

Windows (via WSL)

wsl --install -d Ubuntu-22.04

After install and reboot, open Ubuntu and follow the Linux steps.

Docker (any OS)

docker pull ghcr.io/ton-blockchain/acton:1.0.0
alias acton='docker run --rm -v $(pwd):/work -w /work ghcr.io/ton-blockchain/acton:1.0.0'

The alias only persists for the current shell — for permanence add to .bashrc/.zshrc.

Dependencies

# Node.js 22 LTS — needed for frontend templates and acton func2tolk
nvm install 22
nvm use 22

# Sandbox runtime (optional, for cross-runtime tests)
acton extras install sandbox-runtime

Step 2: Project creation

acton new counter
cd counter

Acton asks:

  • Template: tolk-blank (empty Tolk contract) — pick this.
  • Use git: yes
  • Frontend: skip (not needed for tutorial)

Project layout:

counter/
├── Acton.toml            # project manifest
├── contracts/
│   └── counter.tolk      # contract code
├── tests/
│   └── counter.test.tolk # tests in Tolk
├── scripts/
│   └── deploy.tolk       # deploy script
└── README.md

Step 3: Contract code

Open contracts/counter.tolk and replace with:

import "@stdlib/tvm-dicts"

// Op-codes for incoming messages
const OP_INCREMENT: int = 0x7e8764ef;
const OP_RESET: int = 0xa5a4e3d2;

// Storage layout: one cell with a single uint32
struct Storage {
    counter: uint32,
}

// Load storage from C4
fun loadStorage(): Storage {
    val ds = getData().beginParse();
    val counter = ds.loadUint(32);
    return Storage { counter };
}

// Save storage to C4
fun saveStorage(s: Storage): void {
    setData(
        beginCell()
            .storeUint(s.counter, 32)
            .endCell()
    );
}

// Main: handle incoming messages
fun onInternalMessage(msgValue: int, msgFull: cell, msgBody: slice): void {
    // Empty body — skip (plain TON transfer)
    if (msgBody.bitsCount() < 32) {
        return;
    }

    val op = msgBody.loadUint(32);
    val storage = loadStorage();

    if (op == OP_INCREMENT) {
        storage.counter += 1;
        saveStorage(storage);
        return;
    }

    if (op == OP_RESET) {
        storage.counter = 0;
        saveStorage(storage);
        return;
    }

    // Unknown op — bounce
    throw 0xffff;
}

// Get-method: returns current counter
@get
fun getCounter(): uint32 {
    return loadStorage().counter;
}

What’s happening:

  • import "@stdlib/tvm-dicts" — stdlib (not actually used here, but habit-forming).
  • struct Storage — data model stored in TVM’s C4 register.
  • loadStorage() / saveStorage() — (de)serialisation.
  • onInternalMessage — entry point for inbound messages.
  • @get fun getCounter() — read-only method exposed via TonAPI.

Build

acton build

If clean — output:

✓ Compiled contracts/counter.tolk → build/counter.fif
✓ Generated build/counter.code.boc (152 bytes)
✓ Generated build/counter.abi.json

Step 4: Tests

Open tests/counter.test.tolk:

import "../contracts/counter.tolk"
import "@stdlib/test"

const OP_INCREMENT: int = 0x7e8764ef;
const OP_RESET: int = 0xa5a4e3d2;

@test
fun testInitialState(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(0, 32).endCell(),
    );
    val result = contract.callGet("getCounter");
    assert(result.asInt() == 0, "Initial counter should be 0");
}

@test
fun testIncrement(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(0, 32).endCell(),
    );

    val msg = beginCell().storeUint(OP_INCREMENT, 32).endCell().beginParse();
    contract.sendInternal(msg, 1_000_000_000); // 1 TON gas

    val result = contract.callGet("getCounter");
    assert(result.asInt() == 1, "After increment, counter should be 1");
}

@test
fun testMultipleIncrement(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(0, 32).endCell(),
    );

    for (i in 0..5) {
        val msg = beginCell().storeUint(OP_INCREMENT, 32).endCell().beginParse();
        contract.sendInternal(msg, 1_000_000_000);
    }

    val result = contract.callGet("getCounter");
    assert(result.asInt() == 5, "After 5 increments, counter should be 5");
}

@test
fun testReset(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(42, 32).endCell(),
    );

    val msg = beginCell().storeUint(OP_RESET, 32).endCell().beginParse();
    contract.sendInternal(msg, 1_000_000_000);

    val result = contract.callGet("getCounter");
    assert(result.asInt() == 0, "After reset, counter should be 0");
}

@test
fun testUnknownOpBounces(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(0, 32).endCell(),
    );

    val msg = beginCell().storeUint(0xdeadbeef, 32).endCell().beginParse();
    val txResult = contract.sendInternal(msg, 1_000_000_000);

    assert(txResult.exitCode == 0xffff, "Unknown op should throw 0xffff");
}

Run:

acton test

Should print:

✓ testInitialState (1ms)
✓ testIncrement (2ms)
✓ testMultipleIncrement (5ms)
✓ testReset (2ms)
✓ testUnknownOpBounces (2ms)

5 tests passed, 0 failed in 12ms

Step 5: Mutation testing

Mutation testing measures whether your tests actually exercise the contract. Acton auto-modifies code (+= 1-= 1, ==!=, removes throw) and reruns all tests — if a test doesn’t break under the modification, your coverage is insufficient.

acton test --mutate

Output:

Running mutation tests on contracts/counter.tolk...

Mutation 1: line 33 `storage.counter += 1` → `storage.counter -= 1`
  ✓ Caught by testIncrement (counter became -1, expected 1)

Mutation 2: line 38 `storage.counter = 0` → `storage.counter = 1`
  ✓ Caught by testReset (counter became 1, expected 0)

Mutation 3: line 45 `throw 0xffff` → removed
  ✓ Caught by testUnknownOpBounces (no throw, exitCode=0)

Mutation 4: line 28 `if (msgBody.bitsCount() < 32)` → `if (msgBody.bitsCount() > 32)`
  ⚠ Survived — no test covers empty-body case

3/4 mutations caught (75% coverage). Add tests for surviving mutations.

Survived = gap in tests. Add:

@test
fun testEmptyBodyIgnored(): void {
    val contract = TestContract.deploy(
        counter.code,
        beginCell().storeUint(5, 32).endCell(),
    );

    val emptyMsg = beginCell().endCell().beginParse();
    contract.sendInternal(emptyMsg, 1_000_000_000);

    val result = contract.callGet("getCounter");
    assert(result.asInt() == 5, "Empty body should not modify counter");
}

Rerun acton test --mutate → 4/4 (100%) coverage.

Step 6: Fuzzing

Fuzzing throws random inputs at the contract to find edge cases:

acton test --fuzz

Acton generates 1000+ random inputs:

  • Random op-codes (including invalid)
  • Random msgValue (0 to huge)
  • Empty/malformed slices
  • Maximum-size cells

If the contract throws unexpectedly or consumes anomalous gas — fuzzing flags it.

Step 7: Testnet deploy

You need a testnet wallet with balance. Grab one for free via Testnet TON Bot.

# Store mnemonic in .env (do NOT commit!)
echo "DEPLOYER_MNEMONIC='word1 word2 ... word24'" >> .env

# Deploy
acton script scripts/deploy.tolk --net testnet

scripts/deploy.tolk:

import "../contracts/counter.tolk"

@script
fun deploy(): void {
    val deployer = Wallet.fromMnemonic(env("DEPLOYER_MNEMONIC"));
    val initialData = beginCell().storeUint(0, 32).endCell();

    val deployed = deployer.deployContract(
        counter.code,
        initialData,
        1_000_000_000, // 1 TON gas
    );

    print("Contract deployed at:");
    print(deployed.address.toString());
}

On success — the address prints. Open it on testnet.tonviewer.com:

EQA7ml...

Step 8: Interact via TonAPI

From TypeScript/Node:

import { TonApiClient } from '@ton-api/client';
import { TonClient, Address, beginCell, internal, WalletContractV4 } from '@ton/ton';
import { mnemonicToWalletKey } from '@ton/crypto';

const CONTRACT_ADDRESS = 'EQA7ml...'; // from deploy

async function callIncrement() {
  const tonapi = new TonApiClient({ apiKey: process.env.TONAPI_KEY });
  const tonClient = new TonClient({
    endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC',
  });

  const key = await mnemonicToWalletKey(process.env.DEPLOYER_MNEMONIC!.split(' '));
  const wallet = WalletContractV4.create({
    workchain: 0,
    publicKey: key.publicKey,
  });
  const contract = tonClient.open(wallet);

  // Op-code 0x7e8764ef = increment
  const body = beginCell().storeUint(0x7e8764ef, 32).endCell();

  await contract.sendTransfer({
    seqno: await contract.getSeqno(),
    secretKey: key.secretKey,
    messages: [
      internal({
        to: Address.parse(CONTRACT_ADDRESS),
        value: '0.05',
        body,
      }),
    ],
  });

  console.log('Increment sent. Waiting for confirmation...');
  await new Promise(r => setTimeout(r, 30_000));

  const counter = await tonapi.blockchain.execGetMethodForBlockchainAccount(
    CONTRACT_ADDRESS,
    'getCounter',
  );
  console.log('New counter value:', counter.decoded);
}

callIncrement().catch(console.error);

After running — tonviewer shows two transactions (wallet → contract), and counter increments by 1.

Step 9: Retrace (analysing real transactions)

To study how a contract exploit works, or just dissect a transaction:

acton retrace 7ef9b0a3... --net testnet --debug

Debugger mode opens:

  • Step-into every TVM instruction
  • Inspect stack, registers, gas at each step
  • Breakpoints on specific op-codes

Game-changer for security audits — pre-Acton this didn’t exist on TON.

What’s next

Workflow for any new contract:

  1. acton new <name> --template tolk-blank
  2. Write the contract code
  3. acton build
  4. Write tests, acton test
  5. acton test --mutate until 100% coverage
  6. acton test --fuzz until no surprises
  7. acton check for static analysis (29 lint rules)
  8. acton script scripts/deploy.tolk --net testnet
  9. After testnet check → --net mainnet

Each step is faster than the equivalent on the old stack. 100 tests run in seconds, not minutes; mutation testing works on any contract; deploy is one command.

Bottom line

Acton isn’t just a new CLI — it’s a quality leap in TON developer experience. Install → first contract → testnet deploy in 2 hours, mutation testing and fuzzing out of the box, retrace for transaction forensics. First time trying TON? — start with Acton, skip the old stack.

The first-mover window for projects on TON is open 2-4 months after release (May 2026 — August 2026). Grants, internships, hiring — all in active phase. Good moment to enter.

Frequently asked

If you already have programming and hot-reload-dev experience — installing Acton + first contract + tests takes 1.5-2 hours. Learning Tolk syntax (close to Rust + C) — another 2-3 hours before a serious contract. Mutation testing and retrace come on the third-fourth contract, not the first. Full adaptation (writing production contracts from scratch) — ~40-60 hours of practice.
Speed: Acton tests run on a Rust TVM emulator that's an order of magnitude faster than the JS @ton/sandbox. A large project (10k tests) on Blueprint took 90 seconds; on Acton — 4 seconds. Integration: mutation testing, fuzzing, debugger, formatter, mainnet-tx retrace — one CLI. Quality: every command is typed and tested, fewer regressions.
macOS (ARM64 + x86_64), Linux GNU x86_64 — native. Windows — only via WSL (Ubuntu 20.04+). A native Windows binary isn't planned for mid-2026 (the Rust toolchain depends on POSIX too much). The Docker image ghcr.io/ton-blockchain/acton:1.0.0 works on any OS with Docker, Windows + Docker Desktop included.
Acton supports FunC (compiles through the old backend), but new projects go Tolk. Tact with Acton works partially — not all features (mutation testing, fuzzing) are fully compatible with Tact projects. First time? — pick Tolk: the language is Rust-like, clean syntax, clear errors. An existing FunC codebase can be auto-converted 80-90% via `acton func2tolk`.
Sandbox is a Rust-based TVM emulator that runs your contract in isolation for testing without a real blockchain. Through sandbox you can: send a message, check a get-method, measure gas, inspect resulting transactions. Blueprint had @ton/sandbox (JS); Acton ships a built-in native sandbox.
Yes. After the Acton v1.0 release (May 2026), the TON Foundation is especially eager to fund: Acton plugins, IDE extensions (VS Code / IntelliJ), educational projects (tutorials, videos), indexers on top of the ton-indexer crate, deployment shortcuts (one-click deploys for popular templates). Submit via ton-society/grants-and-bounties on GitHub. First-grant size usually $5-15K.
Retrace = `acton retrace <TX_HASH>` — takes a real mainnet/testnet transaction and reconstructs it locally with a debugger, breakpoints, step-into. Main use: dissecting suspicious transactions (hacks, exploits), incident forensics. Pre-Acton this didn't exist on TON; devs maintained their own script stacks. Retrace is a game-changer for security audits and learning.

Related