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
Contents18sections
- What we’re building
- Step 1: Install Acton
- macOS / Linux
- Windows (via WSL)
- Docker (any OS)
- Dependencies
- Step 2: Project creation
- Step 3: Contract code
- Build
- Step 4: Tests
- Step 5: Mutation testing
- Step 6: Fuzzing
- Step 7: Testnet deploy
- Step 8: Interact via TonAPI
- Step 9: Retrace (analysing real transactions)
- What’s next
- Useful links
- Bottom line
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) andop::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’sC4register.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:
acton new <name> --template tolk-blank- Write the contract code
acton build- Write tests,
acton test acton test --mutateuntil 100% coverageacton test --fuzzuntil no surprisesacton checkfor static analysis (29 lint rules)acton script scripts/deploy.tolk --net testnet- 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.
Useful links
- ACTON Foundry full guide — overview of every command
- Tolk language introduction — syntax and types
- TON Foundation grants — developer grants
- Acton docs — official documentation
- Testnet Giver — free testnet TON
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
How long does this tutorial take if I know TypeScript but not TON?
What makes Acton fundamentally better than Blueprint + Sandbox?
Which OS does Acton support?
Do I need to learn Tolk, or can I start with FunC/Tact?
What's sandbox in the Acton context?
Can I get a TON Foundation grant via Acton work?
What is retrace and what's it for?
Related
- BasicsMay 14, 2026
Acton v1.0 — Foundry for TON: the complete guide (2026)
Acton v1.0 from TON Foundation shipped on May 11, 2026: a single Rust CLI that replaces blueprint, sandbox, Misti and func.
- BasicsDec 23, 2025
TON for developers: FunC, Tact and Tolk in 2026
The smart contract languages used on TON in 2026 — FunC, Tact, Tolk. Which to pick as a beginner, how they differ, what tooling you need and where to learn.
- BasicsMay 14, 2026
Tolk: TON smart contract language — intro for FunC/Tact devs
Tolk is the next-gen TON smart contract language and Acton's default. TypeScript-style syntax, strong typing, pattern matching.
- NewsMar 23, 2026
TON Foundation: who actually runs the blockchain in 2026
A breakdown of TON Foundation — structure, key people, the split from Telegram, the roles of Max Crown and Steve Yun, the rise of TON Strategy Co.
- BasicsFeb 16, 2026
TON mainnet vs testnet: what they are and how to switch
How TON mainnet differs from testnet, how to switch Tonkeeper to testnet, where to get free testnet TON and why developers and curious users care about the.
- BasicsMay 21, 2026
TON SDKs Compared: tonweb vs ton-core vs tonconnect-sdk (2026)
TypeScript SDKs for TON in 2026: tonweb (legacy), @ton/ton + @ton/core (recommended), @tonconnect/sdk. When to pick which and why.