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 --template tolk-blank- Write the contract code
acton build- Write tests,
acton test -
acton test --mutateuntil 100% coverage -
acton test --fuzzuntil no surprises -
acton 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.

Top comments (0)