Skip to content

Contracts

Contracts in Tact are similar to classes in popular object-oriented languages, except that their instances are deployed on the blockchain and they can’t be passed around like Structs and Messages.

Self-references

Contracts and traits have a built-in identifier self, which is used for referring to their fields (persistent state variables and constants) and methods (internal functions):

contract Example {
// persistent state variables
foo: Int;
init() {
self.foo = 42; // <- referencing variable foo through self.
}
}

Structure

Each contract can contain:

Inherited traits, with

Contracts can inherit all the declarations and definitions from traits and override some of their default behaviours. In addition to that, every contract and trait implicitly inherits the special BaseTrait trait.

To inherit a trait, specify its name after the keyword with in contract’s declaration. To inherit multiple traits at once, specify their names in a comma-separated list with an optional trailing comma.

trait InheritMe {}
trait InheritMeToo {}
// A contract inheriting a single trait
contract Single with InheritMe {}
// A contract inheriting multiple traits
contract Plural with
InheritMe,
InheritMeToo, // trailing comma is allowed
{}

As traits are not allowed to have init() function, a contract inheriting a trait with any persistent state variables declared must initialize them by providing its own init() function.

trait Supe { omelander: Bool }
contract Vot with Supe {
init() {
self.omelander = true;
}
}

If declared or defined in a trait, internal functions and constants can be marked as virtual or abstract and overridden in contracts inheriting from the trait.

Supported interfaces, @interface(…)

It’s hard to figure out what a contract does and what receivers and getters it has without looking at its source code. Sometimes the source is unavailable or inaccessible, and all that’s left is to try to disassemble the contract and introspect it that way, which is a very messy and error-prone approach with diminishing returns and no real reproducibility.

In order to resolve this issue, an OTP-001: Supported Interfaces was created. In accordance to it, Tact contracts can report the list of supported interfaces as a return value of a special supported_interfaces getter. That getter is accessible off-chain using any TON Blockchain explorer — one just needs to specify supported_interfaces as a method to execute and get a list of hexadecimal values in return.

These hexadecimal values are truncated to the first 128 bit of SHA-256 hashes of the original String values of the supported interfaces. The first value in this list must be equal to 0x5cec3d5d2cae7b1e84ec39d64a851b66\mathrm{0x5cec3d5d2cae7b1e84ec39d64a851b66} in hexadecimal notation, which is the first half of the SHA-256 hash for "org.ton.introspection.v0". If the first value is wrong, you must stop trying to introspect the contract, as it doesn’t conform to the Supported Interfaces proposal.

To declare support of a certain interface, add one or more @interface("…") attributes right before contract and trait declarations:

@interface("His name is")
@interface("John")
contract SeeNah with Misc {
// ...
}
@interface("name_of_your_org - miscellaneous")
trait Misc {
// ...
}

Tact has a small set of interfaces provided under specific conditions:

Some traits in standard libraries define their interfaces too:

To enable supported_interfaces getter generation and use @interface() attribute in your Tact contracts, modify a tact.config.json file in the root of your project (or create it if it didn’t exist yet), and set the interfacesGetter property to true.

If you’re working on a Blueprint-based project, you can enable supported_interfaces in the compilation configs of your contracts, which are located in a directory named wrappers/:

wrappers/YourContractName.compile.ts
import { CompilerConfig } from '@ton/blueprint';
export const compile: CompilerConfig = {
lang: 'tact',
target: 'contracts/your_contract_name.tact',
options: {
interfacesGetter: true, // ← that's the stuff!
}
};

In addition to that, tact.config.json may still be used in Blueprint projects. In such cases values specified in tact.config.json act as default unless modified in the wrappers/.

Persistent state variables

Contracts can define state variables that persist between contract calls, and thus commonly referred to as storage variables. Contracts in TON pay rent in proportion to the amount of persistent space they consume, so compact representations via serialization are encouraged.

contract Example {
// persistent state variables
val: Int; // Int
val32: Int as uint32; // Int serialized to an 32-bit unsigned
mapVal: map<Int, Int>; // Int keys to Int values
optVal: Int?; // Int or null
}

State variables must have a default value or be initialized in the init() function that runs once on deployment of the contract. The only exception are persistent state variables of type map<K, V>, since they are initialized empty by default.

The default value of state variables is assigned before any values could be assigned in the init() function.

contract Example {
// persistent state variables
var1: Int = 0; // initialized with default value 0
// constructor function
init() {
self.var1 = 42; // overrides the default to 42
}
}

As the contract state is updated at the very end of the compute phase of the transaction, intermediate assignments of Int values that exceed the limits specified by serialization formats won’t fail immediately. Instead, such assignments would cause an exit code 5 only after all statements have been executed.

This is to be expected because the integers in the temporary TVM memory, which is used to process the compute phase, always have 257257 bits and are capable of holding values in the inclusive range from 2256-2^{256} to 22561.2^{256} - 1.

contract DeRanged {
// Persistent state variables
var: Int as uint8; // cannot store values outside the 0-255 range
init() {
self.var = -1; // this won't fail immediately
self.var = 500; // and that won't fail right away either
} // only here, at the end of the compute phase,
// would there be an error thrown with an exit code 5: Integer out of range
}

In the end, this means that you cannot rely on intermediate out-of-bounds checks, because there are none at TVM runtime, and only the last assignment of each state variable is used to update its persistent state value.

contract Zero {
// Persistent state variables
var: Int as uint8; // cannot store values outside the 0-255 range
var2: Int as uint4; // cannot store values outside the 0-15 range
init() {
self.var = -1; // this won't fail
self.var = 0; // and this is in the range of `uint8`
self.var2 = -1; // this won't fail
self.var2 = 15; // and this is in the range of `uint4`
} // no errors, and now `self.var` is 0 and `self.var2` is 15
}

Contract constants

Unlike variables, constants cannot change. Their values are calculated in compile-time and cannot change during execution.

There isn’t much difference between constants defined outside of a contract (global constants) and inside the contract (contract constants). Those defined outside can be used by other contracts in your project.

Constant initializations must be relatively simple and only rely on values known during compilation. If you add two numbers for example, the compiler will calculate the result during build and put the result in your compiled code.

You can read constants both in receivers and in getters.

Unlike contract variables, contract constants don’t consume space in persistent state. Their values are stored directly in the code Cell of the contract.

// global constants are calculated in compile-time and cannot change
const GlobalConst1: Int = 1000 + ton("42") + pow(10, 9);
contract Example {
// contract constants are also calculated in compile-time and cannot change
const ContractConst1: Int = 2000 + ton("43") + pow(10, 9);
// contract constants can be an easy alternative to enums
const StateUnpaid: Int = 0;
const StatePaid: Int = 1;
const StateDelivered: Int = 2;
const StateDisputed: Int = 3;
get fun sum(): Int {
// access constants from anywhere
return GlobalConst1 + self.ContractConst1 + self.StatePaid;
}
}

Read more about constants on their dedicated page: Constants.

Constructor function init()

On deployment of the contract, the constructor function init() is run.

If a contract has any persistent state variables without default values specified, it must initialize them in this function.

contract Example {
// persistent state variables
var1: Int = 0; // initialized with default value 0
var2: Int; // must be initialized in the init() function
var3: Int = 7; // initialized with default value 7
// constructor function
init() {
self.var2 = 42;
self.var3 = 32; // overrides the default to 32
}
}

If a contract doesn’t have any persistent state variables, or they all have their default value specified, it may omit the init() function declaration altogether. That’s because unless explicitly declared, the empty init() function is present by default in all contracts.

The following is an example of a valid empty contract:

contract IamEmptyAndIKnowIt {}

For your convenience, parameter list of init() can have a trailing comma:

contract TheySeeMeTrailing {
init(
param1: Int,
param2: Int, // trailing comma is allowed
) {
// ...
}
}

Getter functions

Getter functions are not accessible from other contracts and exported only to off-chain world.

Additionally, getters cannot modify the contract’s state variables, only read their values and use them in expressions.

contract HelloWorld {
foo: Int;
init() {
self.foo = 0;
}
// getter function with return type Int
get fun foo(): Int {
return self.foo; // can't change self.foo here
}
}

Read more about them in their dedicated section: Getter functions

Receiver functions

Receiver functions in Tact can be one of the following three kinds:

  • receive(), which receive internal messages (from other contracts).
  • bounced(), which are called when outgoing message from this contract has bounced back.
  • external(), which don’t have a sender and can be sent by anyone in the world.
message CanBounce {
counter: Int;
}
contract HelloWorld {
counter: Int;
init() {
self.counter = 0;
}
get fun counter(): Int {
return self.counter;
}
// internal message receiver, which responds to a string message "increment"
receive("increment") {
self.counter += 1;
// sending the message back to the sender
send(SendParameters{
to: sender(),
value: 0,
mode: SendRemainingValue | SendIgnoreErrors,
body: CanBounce{counter: self.counter}.toCell(),
});
}
// bounced message receiver, which is called when the message bounces back to this contract
bounced(src: bounced<MsBounced>) {
self.counter = 0; // reset the counter in case message bounced
}
// external message receiver, which responds to off-chain message "hello, it's me"
external("hello, it's me") {
// can't be replied to as there's no sender!
self.counter = 0;
}
}

Naming a parameter of the receiver function with an underscore _ makes its value considered unused and discarded. This is useful when you don’t need to inspect the message received and you only want it to convey a specific opcode:

message(42) UniverseCalls {}
contract Example {
receive(_: UniverseCalls) {
// Got a Message with opcode 42
}
}

Internal functions

These functions behave similarly to private methods in popular object-oriented languages — they’re internal to contracts and can be called by prefixing them with a special identifier self. That’s why internal functions can sometimes be referred to as “contract methods”.

Internal functions can access the contract’s persistent state variables and constants.

They can only be called from receivers, getters and other internal functions, but not from other contracts or init().

contract Functions {
val: Int = 0;
// this contract method can only be called from within this contract and access its variables
fun onlyZeros() {
require(self.val == 0, "Only zeros are permitted!");
}
// receiver function, which calls the internal function onlyZeros
receive("only zeros") {
self.onlyZeros();
}
}