func2tolk
Overview
Port a legacy FunC smart contract to Tolk while preserving:
- TL-B layout (storage and message bodies)
- opcode numbers and error codes
- observable behavior (including bounce handling and send modes)
Prefer Acton as the development framework (build, wrappers, native Tolk tests, scripts, deploy, verify).
Use acton func2tolk as a first-pass converter when it accelerates the job, then treat the output as a draft to audit and refactor into idiomatic, testable Tolk.
For common mappings and gotchas, open references/porting-checklist.md. For public idiomatic examples and original FunC baselines, open references/repo-examples.md.
Non-negotiable rules (MUST follow)
These rules are mandatory for every FunC -> Tolk porting run.
- MUST follow the idioms in this skill and in:
https://docs.ton.org/languages/tolk/idioms-conventions
- MUST use typed message schemas + union dispatch (
type Allowed...+lazy ...fromSlice(...)+match) for known opcode families. - MUST use structs for storage/message decoding instead of imperative field-by-field parsing in business logic.
- MUST use typed maps (
map<K, V>) and typed helpers in core logic; raw dict parsing is allowed only at boundaries and must be wrapped immediately. - MUST keep TL-B compatibility constraints explicit when deviating from idiomatic code (for example:
UnsafeBodyNoRef,any_address?, legacy inline-body quirks). - MUST open and apply
references/porting-checklist.mdduring the run. - MUST NOT end a porting run before completing the mandatory completion gate in this file.
Forbidden unless explicitly justified by compatibility:
- manual opcode
if/elseifladders for messages that can be modeled as tagged structs + union +match - ad-hoc message parsing (
load_uint/load_msg_addr/...) in domain logic where a typed struct is feasible - raw
udict_*operations spread through core business branches instead of typed map helpers - finishing the run without a checklist-based self-audit
Portable references and example corpus
- Skill references in this skill directory:
references/porting-checklist.mdreferences/repo-examples.md
- Primary public style oracle:
https://github.com/ton-blockchain/acton-contracts- Each contract suite contains Acton/Tolk contracts, native tests, scripts, generated wrappers, benchmark snapshots where relevant, and a README linking to the original FunC implementation.
- High-value Acton/Tolk suites to study:
jetton-v2paired withhttps://github.com/ton-blockchain/jetton-contract/tree/jetton-2.0nftpaired withhttps://github.com/ton-blockchain/nft-contractw5paired withhttps://github.com/ton-blockchain/wallet-contract-v5dnspaired withhttps://github.com/ton-blockchain/dns-contractmultisig-v2paired withhttps://github.com/ton-blockchain/multisig-contract-v2highload-v3paired withhttps://github.com/ton-blockchain/highload-wallet-contract-v3notcoinpaired withhttps://github.com/OpenBuilders/notcoin-contractconfigpaired withhttps://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/config-with-ownable-params.fcelectorpaired withhttps://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/elector-code.fc
counterinacton-contractshas no original FunC counterpart; use it only for minimal Acton project mechanics.- Acton docs:
https://github.com/ton-blockchain/acton/tree/master/docs/content/docs- otherwise use
acton --helpand the official hosted docs
- Tolk idioms and conventions (docs-first style target):
https://docs.ton.org/languages/tolk/idioms-conventions
- Before running
acton ...,cdto the directory that containsActon.toml(Acton resolves contracts/config relative to it).
Porting workflow (Acton-first)
1) Identify the behavioral contract
- Locate FunC entrypoints (
recv_internal,recv_external,run_ticktock, getters) and list handled opcodes. - Extract invariants to preserve:
- exact storage layout (bit widths, field order, presence/absence of refs)
- message TL-B layout (including
Eitherpayloads andMaybefields) - gas/reserve behavior and send modes
- Treat existing JS/TS tests as the spec; translate them into native Tolk tests as you port.
2) Set up Acton project structure
- Scaffold or reuse a project:
acton new PROJECT --template empty|counter|jetton
- Define contracts in
Acton.tomlunder[contracts.*]. - Use
[mappings]to avoid deep relative imports (mirror the@stdlib/...import style). - If the system is multi-contract (Jetton minter + wallet, collection + item, etc.), model dependencies in
Acton.tomland useacton build --graphto sanity-check the dependency DAG.
3) Translate storage and TL-B types first
- Split types into files importable by tests/wrappers (avoid putting message/storage structs inside the contract entrypoint file):
errors.tolk— error codesmessages.tolk— message TL-B structs and union typesstorage.tolk— persistent structs and helpers
- Model contract data as
struct Storage { ... }(and related structs). - Implement storage helpers:
fun Storage.load() { return Storage.fromCell(contract.getData()) }fun Storage.save(self) { contract.setData(self.toCell()) }
- If FunC has “maybe initialized” storage, model it explicitly (for example a wrapper that starts parsing a
slice, checks remaining refs, and then parses either Initialized or NotInitialized variants).
4) Translate message bodies using tagged structs
- Replace FunC
op::...+ manual parsing with tagged structs:struct (0x...) SomeMessage { ... }
- Create a union type for dispatch:
type AllowedMessage = A | B | C
- Parse using lazy decoding:
val msg = lazy AllowedMessage.fromSlice(in.body);
- To match legacy encodings, prefer correct types over ad-hoc bit twiddling:
RemainingBitsAndRefsfor “remainder” payloads / snaked formatsCell<T>for ref payloadsany_address?when the legacy encoding is notaddress?-compatible
5) Implement entrypoints in Acton style
- Internal messages:
fun onInternalMessage(in: InMessage) { ... }
- Bounced messages:
fun onBouncedMessage(in: InMessageBounced) { ... }- Use it to restore balances/supply on failed sends (common in Jettons).
- External messages (wallets, signatures):
fun onExternalMessage(inMsgBody: slice) { ... }
- Prefer
matchfor dispatch; decide explicitly whether to ignore unknown/empty bodies or throw0xFFFF. - If you must handle bounces manually inside
onInternalMessage, use compiler policies (for example@on_bounced_policy("manual")) and document why.
6) Replace low-level message building
- Prefer
createMessage({ ... })+.send(SEND_MODE_...)over manualbuilderencoding. - Keep send modes and bounce flags consistent with FunC behavior.
- Prefer
@stdlib/gas-paymentshelpers when porting contracts that rely onraw_reserve,accept_message, storage fee reservation, etc.
7) Migrate tests (JS/TS → native Tolk)
- Write Acton tests in
tests/*.test.tolk. - Use
net.treasury("name")to create funded emulated accounts. - Use
expect(res)matchers on returned transaction lists. - Generate wrappers (recommended):
acton wrapper CONTRACT_ID --test
- Keep shared types in a separate file (for example
contracts/types.tolk) so wrappers/tests can import them without importing contract entrypoints (avoids duplicateonInternalMessagedefinitions).
8) Iterate with Acton tooling
- Build:
acton build(add--clear-cachewhen debugging). - Test:
acton test(use--filter,--coverage,--debugas needed). - Debug compilation differences:
acton disasm(use--source-mapwhen available). - Dry-run deployment logic locally:
acton script scripts/deploy.tolk(no real TON spent). - Broadcast only after local success:
acton script scripts/deploy.tolk --broadcast --net testnet|mainnet
9) Verify on-chain
- After deployment:
acton verify CONTRACT_ID --address EQ... --net testnet|mainnet
High-signal FunC -> idiomatic Tolk patterns (from acton-contracts)
Use this section as the default translation strategy unless compatibility constraints force lower-level code.
A) Opcode dispatch: replace if (op == ...) chains with typed unions + match
FunC baseline:
op = in_msg_body~load_uint(32); if (op == op::x) { ... } if (op == op::y) { ... }
Idiomatic Tolk:
- declare tagged message structs in
messages.tolk - group them into a union (
type AllowedMessage = A | B | C) - parse lazily and dispatch with
match
type AllowedMessage = Transfer | Burn | TopUp
fun onInternalMessage(in: InMessage) {
val msg = lazy AllowedMessage.fromSlice(in.body);
match (msg) {
Transfer => { ... }
Burn => { ... }
TopUp => { ... }
else => throw 0xFFFF
}
}
Why this is better:
- preserves opcode compatibility while eliminating manual parse drift
- keeps branch-local parsing strongly typed
- scales better for large opcode sets (wallet-v5/vesting/telemint style)
B) Parse bodies into structs, not ad-hoc loads
Prefer:
struct (0x...)for externally visible opcodes- nested structs for grouped payloads (
TransferOwnershipData,MsgInner, etc.) Cell<T>when payload is explicitly by-referenceRemainingBitsAndRefsfor true remainder payloads / snake / either wrappers
Avoid:
- repeated
load_uint/load_msg_addr/load_refin business logic when a struct can model the same layout.
C) Use dedicated messages.tolk modules for inbound formats
Pattern used broadly in ports:
- keep all incoming message schemas in
messages.tolk - import message types into contract entrypoints
- define
Allowed...unions near message definitions
This keeps entrypoint files focused on behavior, not byte parsing.
D) Storage translation: tuple loaders -> typed storage structs + methods
FunC baseline:
(a, b, c) load_data()andsave_data(a, b, c)
Idiomatic Tolk:
struct Storage { ... }Storage.load()/Storage.save()- keep TL-B layout identical (field order + ref/inline shape)
struct Storage { totalSupply: coins adminAddress: address? }
fun Storage.load() { return Storage.fromCell(contract.getData()) }
fun Storage.save(self) { contract.setData(self.toCell()) }
E) Model multi-shape state explicitly (initialized vs not initialized)
Common in NFT/DNS/Telemint items:
- FunC checks
slice_bits() > 0or ref presence to decide shape.
Idiomatic Tolk:
- loader wrapper + explicit parsers:
startLoading...().isInitialized().parseNotInitialized().parseInitialized()
This keeps state transitions explicit and avoids brittle remainder parsing.
F) Dictionaries: replace raw udict_* plumbing with typed maps
FunC baseline:
- manual
udict_get?,udict_set_ref,udict_delete?,udict_delete_get_min
Idiomatic Tolk:
map<K, V>aliases + typed helpers (exists,set,delete,findFirst,iterateNext)- convert low-level dicts only at boundaries (
createMapFromLowLevelDict(...))
Examples:
- DNS records:
type DnsRecords = map<uint256, cell> - Wallet extensions:
map<uint256, bool> - Query bitmaps:
map<uint13, Bitmap>
G) Prefer createMessage(...) over manual begin_cell() message assembly
Default:
- build outgoing messages with
createMessage({ ... }) - send via
.send(SEND_MODE_...)
Keep manual builders only when:
- emulating historical quirks exactly
- forcing exact inline/no-ref body layout
H) External signature flows: parse suffixes/remainders deliberately
For wallet-style externals:
- separate signature from signed payload (
getLastBits/removeLastBitsor explicit field ordering) - validate hash/signature first
- update replay state (
seqno/ query maps), persist, commit - then execute actions/messages
This mirrors hardened wallet-v5/highload patterns.
I) Bounce handling: prefer onBouncedMessage with typed bounced unions
FunC baseline:
- bounce branch in
recv_internal+ manualload_uint
Idiomatic Tolk:
onBouncedMessage(in: InMessageBounced)- optional union for bounced op subset:
type BounceOpToHandle = A | B
- restore balances/supply from typed parsed message
J) Compatibility levers to preserve legacy TL-B/behavior
Use these intentionally when matching FunC tests:
UnsafeBodyNoRef { forceInline: ... }for no-ref body compatibilityany_address?when legacy layout is broader thanaddress?- explicit
(Either Cell ^Cell)remainder validation helpers for payload correctness - keep unknown-op behavior identical (
throw 0xFFFFvs ignore empty/body)
K) Domain-specific recipes extracted from real ports
Jetton family:
AllowedMessageToMinter/AllowedMessageToWallet- typed bounce restore (
InternalTransferStep | BurnNotification...) - typed forward payload (
RemainingBitsAndRefs) + validator helper
NFT + DNS items:
- explicit uninitialized->initialized transition path
- ownership transfer payload as nested struct
- map-based content records instead of imperative dict bit twiddling
Highload wallet:
- validate minimal shape early, then typed parse
- model replay bitmaps as typed map + helper methods
- typed validation for outbound raw message shape before forwarding
Vesting / policy wallets:
- whitelist maps as typed
map<address, ()> - validate allowed opcodes using union parsing +
match(instead of numeric opcode allowlists spread across branches)
Mandatory completion gate (before ending any porting run)
Do not end the run until all items below are checked.
- Checklist was opened and applied:
references/porting-checklist.md
- Dispatch is idiomatic:
- known message families use tagged structs + union +
match
- known message families use tagged structs + union +
- Parsing is typed:
- no avoidable imperative parsing in business branches
- Storage is typed:
Storage.load()/Storage.save()(or explicit initialized/uninitialized loader pattern)
- Dict usage is typed:
map<K,V>+ helpers in logic; boundary raw dict decoding is wrapped
- Compatibility exceptions are documented:
- any non-idiomatic construct is justified as TL-B/behavior parity
- Build/tests status is reported:
- run relevant
acton build/acton test(or report exact blocker)
- run relevant
- Final response includes a short self-audit summary against the gate above.
If any gate item cannot be completed, do not silently finish. Report the blocker and list incomplete gate items explicitly.
Using acton-contracts as a style oracle
Use https://github.com/ton-blockchain/acton-contracts as the default public style oracle for production ports:
- use each suite's README to find the original FunC repo or file
- use the original FunC source as the behavioral baseline, especially for edge cases
- use the matching Acton/Tolk suite as the target style for module layout, typed storage, message unions, wrappers, scripts, tests, and benchmark snapshots
Prefer searching by:
- opcode hex (for example
0xd53276db) - message type name (for example
InternalTransferStep) - storage field name (for example
totalSupply,ownerAddress)
Open references/repo-examples.md for suite mappings, original FunC links, and search targets.
Docs-first lookups
- For Tolk language details and standard library, use TON Docs pages under
languages/tolk/(overview, serialization, standard library, FunC comparison). - For Acton specifics (commands, wrappers, testing, scripts), use Acton docs and
acton --help.
