Smart contracts you can read.
TCCL is the smart-contract language of The Coin. It looks like Python — indentation-based blocks, few keywords — but every value has a declared type, every step costs fuel, and the same code gives exactly the same result on every node.
Strict by design.
Anything ambiguous is rejected before it reaches the network. What runs on-chain is exactly what anyone can read.
Strictly typed
Every constant, state variable, parameter, and local variable
declares its type. There are no implicit conversions: adding an
int to a text is a compile error. A
view cannot change state, and only
payable functions accept TCN.
Deterministic
No floating-point numbers, no clock, no randomness, no network
access. Integers are 128-bit, and any overflow or division by zero
stops execution. The only notion of time is the block height
(height), so every node reaches the same result.
Fuel-limited
Every statement, storage read or write, hash, and payment consumes
fuel. A transaction reserves a limit (max_fuel). If
the fuel runs out or a require fails,
everything is reverted — the value sent is
returned, nothing is stored — and only the fee is charged.
Infinite loops are impossible. Read-only view queries
are free.
Source code on-chain
A deployment transaction carries the source code, not a binary: every node compiles the same text with the same compiler. The contract stores the hash of its source, and anyone can read exactly what will run — in the Explorer, on every contract address.
Get the tools.
You need tccl to check and simulate contracts, and
thecoin-wallet to deploy and call them.
One-line installer
The node installer also installs the
thecoin-wallet wallet and the tccl
developer tool:
curl -fsSL https://the-coin.cloud/install.sh | sudo bash -s --
--yes
Only want to write and test contracts? Add
--no-mine, or use just the binaries: the
tccl run simulator does not need a node. See the
validator guide for everything the
installer does.
Binaries or source
The release packages in
the-coin.cloud/releases contain
thecoind, thecoin-wallet, and
tccl. Or build them with Rust:
git clone https://github.com/LucasBolla94/thecoin
cd thecoin
cargo build --release -p tccl -p thecoin-wallet
./target/release/tccl --version
From an idea to the network, step by step.
The outputs below are real runs of version 0.2 with the counter.tccl example.
Write the contract.
Save it as counter.tccl: a counter anyone can
increase, with an event and two queries.
# The smallest useful contract: a counter anyone can increase.
contract Counter
state count: int
state last_caller: address
event Increased(by: address, amount: int, total: int)
action increment(amount: int):
require amount > 0, "amount must be positive"
require amount <= 100, "at most 100 per call"
count += amount
last_caller = caller
emit Increased(caller, amount, count)
view get() -> int:
return count
view last() -> address:
return last_caller
The first line of code names the contract.
state is data stored on the blockchain (it starts as
zero, empty text, or the zero address); event
describes a record that appears in the transaction receipt and the
explorer; an action changes state and is called by a
transaction; a view only reads and is free.
caller is whoever signed the call.
Check it with tccl check.
The compiler checks types, return paths, and view rules exactly like the network, and prints the interface:
$ tccl check counter.tccl
✔ Counter compiles (484 bytes of source, 323 bytes compiled)
state: count: int, last_caller: address
action increment(amount: int)
view get() -> int
view last() -> address
Simulate it with tccl run.
A local blockchain on your computer, with test accounts
(alice, bob, …) that start with
1,000,000 TCN. State is kept in tccl-state.json.
$ tccl run counter.tccl deploy
deployed Counter at daa436158c1dcdc0242085b6dd9451e33496cbee
ok · fuel used: 2420 · height: 2 · alice balance: 100000000000000 motes
$ tccl run counter.tccl call increment 5
event Increased(by: tcr1zqygp3t5ugluq2uqf7gq60mc9z9t09lpkg42sa, amount: 5, total: 5)
ok · fuel used: 1674 · height: 3 · alice balance: 100000000000000 motes
$ tccl run counter.tccl --from bob call increment 7
event Increased(by: tcr14vqs008pe0yx022r6pneffl2mnn0x2lkzh7452, amount: 7, total: 12)
ok · fuel used: 1674 · height: 4 · bob balance: 100000000000000 motes
$ tccl run counter.tccl view get
result: 12
ok · fuel used: 273 · height: 4 · alice balance: 100000000000000 motes
$ tccl run counter.tccl --from bob call increment 500
FAILED: requirement failed: at most 100 per call · fuel used: 33 (all changes reverted)
error: call failed
Useful options: --from <account>,
--value 2.5tcn (for payable functions),
--height <n>, --state <file>,
and @alice as an address argument. Delete
tccl-state.json to start over.
Deploy it with the wallet.
The wallet compiles the contract, simulates it on the node to
measure the fuel, reserves a margin, shows the fee, and asks for
confirmation (-y skips the prompt). Add
--network testnet while learning. This output was
recorded on a private regtest network (addresses
tcr1…), whose default fees are ten times lower than
mainnet’s:
$ thecoin-wallet -y contract deploy counter.tccl
Fuel: 2420 measured, limit 8146
From: tcr1a9edrkqqhylclwfdaf8sk78zgfkkc0ctvk4awq
Action: deploy contract Counter (484 bytes of TCCL)
Fee: 0.00001948 TCN (643 bytes, priority Normal)
Broadcast OK. txid: 28e99a814f77e26f776274cef2e2e80db1703f15b360690b26d3aef0a03fdd60
Contract address: tcr17kp5hrqhpdkwmukkrsfp6xft6y9l8j02rhxmvz
Options: --max-deposit (the most you accept to lock
as a storage deposit; default 1 TCN), --max-fuel,
--value, and --priority. Arguments for
init go after the file name.
Call an action.
Calls are simulated first, too: if a call would fail, the wallet tells you and sends nothing.
$ thecoin-wallet -y contract invoke tcr17kp5hrqhpdkwmukkrsfp6xft6y9l8j02rhxmvz increment 5
Fuel: 1777 measured, limit 7310
Preview: Increased(by: tcr1a9edrkqqhylclwfdaf8sk78zgfkkc0ctvk4awq, amount: 5, total: 5) (simulated on the current state)
From: tcr1a9edrkqqhylclwfdaf8sk78zgfkkc0ctvk4awq
Action: increment(5) on tcr17kp5hrqhpdkwmukkrsfp6xft6y9l8j02rhxmvz
Fee: 0.00001295 TCN (205 bytes, priority Normal)
Broadcast OK. txid: 04a24a2dd4268cc4fa0ff1d3c54aba9f953f03abc8aad56a9e451a84da4c7e5b
Query it.
Views create no transaction and cost nothing:
$ thecoin-wallet contract view tcr17kp5hrqhpdkwmukkrsfp6xft6y9l8j02rhxmvz get
5
$ thecoin-wallet contract program tcr17kp5hrqhpdkwmukkrsfp6xft6y9l8j02rhxmvz # balance, storage, deposit, interface
$ thecoin-wallet tx <txid> # receipt: success, error, fuel_used, events, return_value
In the Explorer, search for the contract address to see its functions, source code, deposit, the events of each call, and forms to call its views from your browser.
The language in one page.
The full reference, including operators, error messages, and a security checklist, is in TCCL.md ↗ and the cookbook.
Contract layout
# Contract layout: constants, state, events and functions.
contract PiggyBank
const MINIMUM: int = 1 * TCN
state owner: address
state label: text = "Piggy bank"
state deposits: map[address, int]
state amounts: list[int]
event Deposited(who: address, amount: int)
init():
owner = caller
action deposit() payable:
require value >= MINIMUM, "minimum deposit is 1 TCN"
deposits[caller] += value
amounts.push(value)
emit Deposited(caller, value)
action withdraw(amount: int):
require amount > 0 and deposits[caller] >= amount, "insufficient balance"
deposits[caller] -= amount
send(caller, amount)
view balance_of(who: address) -> int:
return deposits[who]
view total_held() -> int:
return balance
fn half(x: int) -> int:
return x / 2
| Declaration | Purpose |
|---|---|
| contract Name | The first line of code; one file is one contract. The name appears in the explorer. |
| const NAME: type = value |
Constant computed at compile time from literals, earlier
constants, operators, and address("tc1…").
|
| state name: type [= value] | Data stored on the blockchain. Only scalar types accept an initial value; maps exist only as state. |
| event Name(field: type, …) |
Record emitted with emit; it appears in the
transaction receipt.
|
| init(…) [payable]: | Optional; runs once, at deployment. |
| action name(…) [-> type] [payable]: |
Entry point called by a transaction; can change state and send
TCN. The return type comes before payable.
|
| view name(…) -> type: | Free, read-only query; must return a value. |
| fn name(…) [-> type]: | Private helper. Helpers are the only functions code can call; entry points cannot be called from code. |
Types
| Type | Example | Notes |
|---|---|---|
| int | 42, -7, 1_000_000, 5 * TCN |
Signed 128-bit integer with overflow checks; /
truncates toward zero. Amounts are in motes:
TCN = 100,000,000.
|
| bool | true, false |
Result of comparisons; and, or,
not. Conditions must be bool.
|
| text | "hello" |
UTF-8; escapes \n \t \" \\; join with
+.
|
| bytes | 0xdeadbeef |
Join with +; b[i] returns an
int.
|
| address | caller, address("tc1…") |
An account or contract; zero_address() is the
empty address.
|
| list[T] | [1, 2, 3] |
Up to 4,096 items in a local list; xs[i],
len(xs), xs.push(x);
xs.pop() on state lists.
|
| map[K, V] | balances[caller] += 1 |
State only; key int, bool,
text, bytes, or
address. A missing key reads as the type’s zero
value; m.has(k), m.remove(k). Maps
cannot be iterated.
|
Default values are never stored: assigning 0,
false, "", empty bytes or list, or the zero
address deletes the storage entry. 5tcn is accepted only
in command-line arguments — in source code write
5 * TCN.
Statements, context, and built-in functions
# Control flow, lists, maps and built-in functions.
contract Examples
state seen: map[bytes, bool]
state heights: list[int]
view sum_even(n: int) -> int:
let total: int = 0
for i in range(0, n):
if i % 2 == 0:
total += i
elif i > 1000:
break
else:
continue
return total
view largest(xs: list[int]) -> int:
require len(xs) > 0, "empty list"
let m: int = xs[0]
for x in xs:
m = max(m, x)
return m
view digest(t: text) -> bytes:
return slice(sha256(t), 0, 4) + blake3(to_bytes(len(t)))
view label(n: int) -> text:
return "value: " + to_text(abs(n))
view signature_ok(key: bytes, msg: bytes, sig: bytes) -> bool:
return verify_ed25519(key, msg, sig)
view address_for_key(key: bytes) -> address:
return address_of(key)
action mark(key: bytes):
require not seen.has(key), "already marked"
seen[key] = true
heights.push(height)
action unmark(key: bytes):
seen.remove(key)
let n: int = 0
while n < 3 and len(heights) > 0:
heights.pop()
n += 1
| Statement | Effect |
|---|---|
| let x: type = value | Local variable; the type and value are mandatory. |
| x = v · x += v · x -= v · x *= v | Assignment; += also joins text and bytes. |
| if / elif / else | Condition (always bool). |
| while cond: | Loop, bounded by the fuel limit. |
| for i in range(a, b): | From a to b − 1. |
| for x in xs: | Iterates over a local or state list. |
| break · continue · pass | Loop control / empty block. |
| return value |
Required on every path when the function declares
-> type.
|
| require cond, "message" | If false, the call fails and every effect is reverted. |
| send(address, amount) | Sends motes from the contract balance. It never runs code at the receiver: contracts cannot call contracts, so there is no re-entrancy. |
| emit Event(…) | Records an event in the receipt. |
| destroy(address) | Actions only: deletes the contract once its lists and maps are empty, and pays its balance and storage deposit to the address. |
| Name | Type / effect |
|---|---|
| caller |
address that signed the call (the zero address
inside views).
|
| value |
int: motes sent with the call (not available in
views).
|
| balance |
int: the contract balance, including
value.
|
| height | int: block height (about one block a minute). |
| self | The contract’s own address. |
| len(x) | Length of a list, text (in bytes), or bytes. |
| sha256(x) · blake3(x) | Hash of bytes or text → 32 bytes. |
| to_bytes(x) | int, bool, text, address, or bytes → bytes. |
| to_text(n) · to_int(b) |
int → decimal text · up to 15 bytes → unsigned
int.
|
| min(a, b) · max(a, b) · abs(n) | Arithmetic on int. |
| slice(b, start, end) | Part of a bytes value. |
| verify_ed25519(key, msg, signature) |
bool; false for malformed input.
|
| ring_verify(ring, msg, signature, key_image) |
bool: linkable ring signature (bLSAG over
Ristretto255), up to 64 keys.
|
| address_of(key) | The Coin address of a 32-byte public key. |
| zero_address() | The empty address. |
Closing a contract
# destroy(): deletes the contract and returns its balance and storage deposit.
contract Temporary
state owner: address
state notes: map[int, text]
init():
owner = caller
action write(n: int, note: text):
require caller == owner, "only the owner"
notes[n] = note
action erase(n: int):
require caller == owner, "only the owner"
notes.remove(n)
action close():
require caller == owner, "only the owner"
destroy(owner)
destroy only works after every map entry and list item
has been removed (scalar variables are cleared automatically). The
balance and the whole storage deposit go to the given address.
Bounded, and priced by work.
Fuel prices are calibrated to roughly 20 ns of CPU per unit on a 2-vCPU server, so a completely full block executes in about a second in the worst case.
| Limit | Value |
|---|---|
| Source code | 48,000 bytes |
| Deploy transaction · other transactions | 64,000 · 16,384 bytes |
| Compiled program | 262,144 bytes |
| Functions · state variables · locals per function | 256 · 256 · 1,024 |
| Call depth | 16 |
| Nesting of blocks, parentheses, types | 32 |
| Operators per precedence level · expression depth | 64 · 128 |
| Arguments per call · events per call | 32 · 64 |
| Text, bytes, or list value · local list items | 65,536 bytes · 4,096 |
| Fuel per transaction | 10,000,000 |
| Fuel per block (mainnet default) | 50,000,000 |
| Fuel per view via the API · in the simulator | 2,000,000 · 5,000,000 |
| Operation | Fuel |
|---|---|
| Statement · expression | 2 · 1 |
| Values read or combined | 1 per 32 bytes |
| Function call | 20 |
| Storage read | 250 |
| Storage write or delete | 400 + 4 per byte |
| sha256 · blake3 | 60 + 20 per 64 bytes |
| send · emit | 300 · 100 + 1 per byte |
| verify_ed25519 | 3,500 + 1 per 64 bytes |
| ring_verify | 5,000 + 10,000 per key |
| destroy | 1,000 |
| Deploy: compile | 5 per source byte |
| Invoke: load the contract | 100 + 1 per 100 bytes of code |
No surprises in the price.
Fee
Paid when you deploy or call, like any transaction: a base amount,
plus the size in bytes, plus the reserved fuel
(max_fuel). It is charged even if the contract fails
after being mined — the work of running it was done.
Fuel
Measures the work. You pay for the limit you reserve, not for what you used, so the wallet measures the call with a simulation and reserves only a margin (measured × 1.3 + 5,000). View queries cost nothing.
Storage deposit
Data on the blockchain uses disk space on every node, so whoever
makes a contract grow locks a deposit per started
kB. It is not spent: it is returned proportionally when space is
freed, and in full on destroy.
The exact formulas (each division rounds up):
deposit = ⌈contract bytes ÷ 1,000⌉ × storage_deposit_per_kb
Worked example: the counter on mainnet
Mainnet defaults: base_fee 1,000 motes,
fee_per_kb 10,000 motes, fee_per_kfuel
1,000 motes, and a storage_deposit_per_kb of 100,000
motes (1 TCN = 100,000,000 motes). Sizes and fuel come from the
deployment above; no congestion (1×). The wallet’s normal priority
pays 25% over the minimum.
| Operation | Calculation | Minimum | Normal priority (+25%) |
|---|---|---|---|
| Deploy (643 bytes, reserves 8,146 fuel) | 1,000 + 6,430 + 8,146 |
15,576 motes 0.00015576 TCN |
19,470 motes 0.0001947 TCN |
| Deposit (323-byte contract → 1 kB) | 1 × 100,000 |
100,000 motes 0.001 TCN · refundable |
— |
increment 5 (205 bytes, reserves 7,310, uses
1,777)
|
1,000 + 2,050 + 7,310 |
10,360 motes 0.0001036 TCN |
12,950 motes 0.0001295 TCN |
view get |
— | free | free |
| For comparison: a simple 159-byte transfer | 1,000 + 1,590 |
2,590 motes 0.0000259 TCN |
3,238 motes 0.00003238 TCN |
After the first call the counter uses 367 bytes — still within 1 kB, so no extra deposit is locked. When blocks fill up, the congestion multiplier raises the minimum fee (at 2×, the deployment costs at least 31,152 motes) and the surcharge is burned rather than paid to the miner. These parameters can change through governance; see the live values in Explorer → Current fees.
Private payments with contract pools.
How it works
The Coin is transparent: balances and transfers are public.
Privacy exists only inside user-written TCCL
contracts that use ring_verify — linkable
ring signatures (bLSAG over Ristretto255).
- Deposit: send the pool’s fixed amount (10 TCN in the example) together with a fresh ring public key.
- Wait: other people deposit the same amount.
- Withdraw: sign with a ring made of several deposits. The contract learns only that one of them signed, never which, and pays a new address. The key image prevents the same deposit from being withdrawn twice.
- Relayer (optional): another account submits the withdrawal and earns a small fee, so the destination address does not need TCN beforehand.
Full example: private_pool.tccl.
In the wallet
Ring keys are derived from your wallet’s recovery phrase. The wallet works with any pool that follows the standard interface:
thecoin-wallet privacy keygen --key 0
thecoin-wallet privacy deposit tc1…pool --key 0
thecoin-wallet privacy status tc1…pool --key 0
thecoin-wallet privacy withdraw tc1…pool --key 0 \
--to tc1…new --ring-size 16
Optional withdrawal flags: --relayer <address>
and --fee <TCN>.
TCCL-PRIV-1 INTERFACE
| Function | Purpose |
|---|---|
| denomination() -> int | fixed deposit amount |
| deposits() -> int | number of deposits |
| key_at(index: int) -> bytes | public key of a deposit |
| is_withdrawn(key_image: bytes) -> bool | already withdrawn? |
| message_for(to, relayer, fee) -> bytes | message to sign |
| deposit(public_key: bytes) payable | deposit action |
| withdraw(to, relayer, fee, members, signature, key_image) | withdrawal action |
Privacy is only as good as the number of deposits in the ring: wait for other people to deposit before withdrawing, withdraw to an address that has never been used, and, if you can, submit the withdrawal from another account or through a relayer (withdrawing from the address that deposited reveals the link — the wallet warns you). Pools are user contracts, not part of the protocol: read the pool’s source code in the explorer before depositing.
Keep building.
TCCL Cookbook
Complete, commented recipes, a security checklist, and the full reference.
PDFLanguage reference
The quick start and complete language reference on GitHub.
TCCL.mdContract API
GET /api/v1/program/{address},
POST /api/v1/program/{address}/view, and
POST /api/v1/tx/simulate for wallets and apps.
Ready-made examples
| File | What it does |
|---|---|
| counter.tccl | The smallest useful contract: a counter with an event. |
| shop.tccl | A shop with per-item prices, commented part by part — a good way to learn every kind of declaration. |
| tip_jar.tccl | A tip jar: anyone sends TCN with a message; only the owner withdraws. |
| token.tccl | A token with a fixed maximum supply, transfers, and allowances. |
| crowdfund.tccl | Crowdfunding with a goal and a deadline in blocks; backers are refunded if the goal is missed. |
| escrow.tccl | Escrow payment with an arbiter and a deadline. |
| savings.tccl | Savings locked until a block height you choose. |
| poll.tccl | A poll with fixed options and registered voters. |
| names.tccl | A name service: human-readable names that point to addresses, rented by the year. |
| treasury.tccl | A treasury with several owners (M-of-N approvals). |
| private_pool.tccl | A private payments pool (TCCL-PRIV-1). |
Every example compiles with tccl check and is exercised by
the test suite (cargo test -p tccl).
Want a node to deploy from?
One command installs the node, the wallet, and the
tccl tool on a Linux server.