Contract Functions
In this section, we are going to be looking at the different types of functions you could encounter in Starknet smart contracts.
Functions can access the contract's state easily via self: ContractState
, which abstracts away the complexity of underlying system calls (storage_read_syscall
and storage_write_syscall
). The compiler provides two modifiers: ref
and @
to decorate self
, which intends to distinguish view and external functions.
1. Constructors
Constructors are a special type of function that only runs once when deploying a contract, and can be used to initialize the state of a contract.
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.entry(owner.address).write(owner.name);
self.total_names.write(1);
self.owner.write(owner);
}
Some important rules to note:
- A contract can't have more than one constructor.
- The constructor function must be named
constructor
, and must be annotated with the#[constructor]
attribute.
The constructor
function might take arguments, which are passed when deploying the contract. In our example, we pass some value corresponding to a Person
type as argument in order to store the owner
information (address and name) in the contract.
Note that the constructor
function must take self
as a first argument, corresponding to the state of the contract, generally passed by reference with the ref
keyword to be able to modify the contract's state. We will explain self
and its type shortly.
2. Public Functions
As stated previously, public functions are accessible from outside of the contract. They are usually defined inside an implementation block annotated with the #[abi(embed_v0)]
attribute, but might also be defined independently under the #[external(v0)]
attribute.
The #[abi(embed_v0)]
attribute means that all functions embedded inside it are implementations of the Starknet interface of the contract, and therefore potential entry points.
Annotating an impl block with the #[abi(embed_v0)]
attribute only affects the visibility (i.e., public vs private/internal) of the functions it contains, but it doesn't inform us on the ability of these functions to modify the state of the contract.
// Public functions inside an impl block
#[abi(embed_v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252, registration_type: RegistrationType) {
let caller = get_caller_address();
self._store_name(caller, name, registration_type);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
self.names.entry(address).read()
}
fn get_owner(self: @ContractState) -> Person {
self.owner.read()
}
fn get_owner_name(self: @ContractState) -> felt252 {
self.owner.name.read()
}
fn get_registration_info(
self: @ContractState, address: ContractAddress
) -> RegistrationInfo {
self.registrations.entry(address).info.read()
}
}
Similarly to the
constructor
function, all public functions, either standalone functions annotated with the#[external(v0)]
or functions within an impl block annotated with the#[abi(embed_v0)]
attribute, must takeself
as a first argument. This is not the case for private functions.
External Functions
External functions are public functions where the self: ContractState
argument is passed by reference with the ref
keyword, which exposes both the read
and write
access to storage variables. This allows modifying the state of the contract via self
directly.
fn store_name(ref self: ContractState, name: felt252, registration_type: RegistrationType) {
let caller = get_caller_address();
self._store_name(caller, name, registration_type);
}
View Functions
View functions are public functions where the self: ContractState
argument is passed as snapshot, which only allows the read
access to storage variables, and restricts writes to storage made via self
by causing compilation errors. The compiler will mark their state_mutability to view
, preventing any state modification through self
directly.
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
self.names.entry(address).read()
}
State Mutability of Public Functions
However, as you may have noticed, passing self
as a snapshot only restricts the storage write access via self
at compile time. It does not prevent state modification via direct system calls, nor calling another contract that would modify the state.
The read-only property of view functions is not enforced on Starknet, and sending a transaction targeting a view function could change the state.
In conclusion, even though external and view functions are distinguished by the Cairo compiler, all public functions can be called through an invoke transaction and can potentially modify the Starknet state. Moreover, all public functions can be called with the starknet_call
RPC method, which will not create a transaction and hence will not change the state.
Warning: This is different from the EVM where a
staticcall
opcode is provided, which prevents storage modifications in the current context and subcontexts. Hence developers should not have the assumption that calling a view function on another contract cannot modify the state.
Standalone Public Functions
It is also possible to define public functions outside of an implementation of a trait, using the #[external(v0)]
attribute. Doing this will automatically generate an entry in the contract ABI, allowing these standalone public functions to be callable by anyone from outside. These functions can also be called from within the contract just like any function in Starknet contracts. The first parameter must be self
.
Here, we define a standalone get_contract_name
function outside of an impl block:
// Standalone public function
#[external(v0)]
fn get_contract_name(self: @ContractState) -> felt252 {
'Name Registry'
}
3. Private Functions
Functions that are not defined with the #[external(v0)]
attribute or inside a block annotated with the #[abi(embed_v0)]
attribute are private functions (also called internal functions). They can only be called from within the contract.
They can be grouped in a dedicated impl block (e.g., in components, to easily import internal functions all at once in the embedding contracts) or just be added as free functions inside the contract module. Note that these 2 methods are equivalent. Just choose the one that makes your code more readable and easy to use.
// Could be a group of functions about a same topic
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn _store_name(
ref self: ContractState,
user: ContractAddress,
name: felt252,
registration_type: RegistrationType
) {
let total_names = self.total_names.read();
self.names.entry(user).write(name);
let registration_info = RegistrationInfo {
name: name,
registration_type: registration_type,
registration_date: starknet::get_block_timestamp(),
};
let mut registration_node = self.registrations.entry(user);
registration_node.info.write(registration_info);
let count = registration_node.count.read();
registration_node.history.entry(count).write(registration_info);
registration_node.count.write(count + 1);
self.total_names.write(total_names + 1);
self.emit(StoredName { user: user, name: name });
}
}
// Free function
fn get_owner_storage_address(self: @ContractState) -> felt252 {
self.owner.__base_address__
}
Wait, what is this
#[generate_trait]
attribute? Where is the trait definition for this implementation? Well, the#[generate_trait]
attribute is a special attribute that tells the compiler to generate a trait definition for the implementation block. This allows you to get rid of the boilerplate code of defining a trait with generic parameters and implementing it for the implementation block. With this attribute, we can simply define the implementation block directly, without any generic parameter, and useself: ContractState
in our functions.
The #[generate_trait]
attribute is mostly used to define private impl blocks. It might also be used in addition to #[abi(per_item)]
to define the various entrypoints of a contract (see next section).
Note: using
#[generate_trait]
in addition to the#[abi(embed_v0)]
attribute for a public impl block is not recommended, as it will result in a failure to generate the corresponding ABI. Public functions should only be defined in an impl block annotated with#[generate_trait]
if this block is also annotated with the#[abi(per_item)]
attribute.
[abi(per_item)]
Attribute
You can also define the entrypoint type of functions individually inside an impl block using the#[abi(per_item)]
attribute on top of your impl. It is often used with the #[generate_trait]
attribute, as it allows you to define entrypoints without an explicit interface. In this case, the functions will not be grouped under an impl in the ABI. Note that when using #[abi(per_item)]
attribute, public functions need to be annotated with the #[external(v0)]
attribute - otherwise, they will not be exposed and will be considered as private functions.
Here is a short example:
#[starknet::contract]
mod ContractExample {
#[storage]
struct Storage {}
#[abi(per_item)]
#[generate_trait]
impl SomeImpl of SomeTrait {
#[constructor]
// this is a constructor function
fn constructor(ref self: ContractState) {}
#[external(v0)]
// this is a public function
fn external_function(ref self: ContractState, arg1: felt252) {}
#[l1_handler]
// this is a l1_handler function
fn handle_message(ref self: ContractState, from_address: felt252, arg: felt252) {}
// this is an internal function
fn internal_function(self: @ContractState) {}
}
}
In the case of #[abi(per_item)]
attribute usage without #[generate_trait]
, it will only be possible to include constructor
, l1-handler
and internal
functions in the trait implementation. Indeed, #[abi(per_item)]
only works with a trait that is not defined as a Starknet interface. Hence, it will be mandatory to create another trait defined as interface to implement public functions.