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 the self: ContractState
object, 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.
Let's consider the NameRegistry contract in Listing
15-4 that we'll be using throughout this chapter:
use starknet::ContractAddress;
#[starknet::interface]
pub trait INameRegistry<TContractState> {
fn store_name(ref self: TContractState, name: felt252);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
}
#[starknet::contract]
mod NameRegistry {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: Map<ContractAddress, felt252>,
total_names: u128,
}
#[derive(Drop, Serde, starknet::Store)]
pub struct Person {
address: ContractAddress,
name: felt252,
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.entry(owner.address).write(owner.name);
self.total_names.write(1);
}
// Public functions inside an impl block
#[abi(embed_v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252) {
let caller = get_caller_address();
self._store_name(caller, name);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
self.names.entry(address).read()
}
}
// Standalone public function
#[external(v0)]
fn get_contract_name(self: @ContractState) -> felt252 {
'Name Registry'
}
// 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) {
let total_names = self.total_names.read();
self.names.entry(user).write(name);
self.total_names.write(total_names + 1);
}
}
// Free function
fn get_total_names_storage_address(self: @ContractState) -> felt252 {
self.total_names.__base_address__
}
}
15-4: Our reference contract for this chapter
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.
use starknet::ContractAddress;
#[starknet::interface]
pub trait INameRegistry<TContractState> {
fn store_name(ref self: TContractState, name: felt252);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
}
#[starknet::contract]
mod NameRegistry {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: Map<ContractAddress, felt252>,
total_names: u128,
}
#[derive(Drop, Serde, starknet::Store)]
pub struct Person {
address: ContractAddress,
name: felt252,
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.entry(owner.address).write(owner.name);
self.total_names.write(1);
}
// Public functions inside an impl block
#[abi(embed_v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252) {
let caller = get_caller_address();
self._store_name(caller, name);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
self.names.entry(address).read()
}
}
// Standalone public function
#[external(v0)]
fn get_contract_name(self: @ContractState) -> felt252 {
'Name Registry'
}
// 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) {
let total_names = self.total_names.read();
self.names.entry(user).write(name);
self.total_names.write(total_names + 1);
}
}
// Free function
fn get_total_names_storage_address(self: @ContractState) -> felt252 {
self.total_names.__base_address__
}
}
#[cfg(test)]
mod tests;
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.
use starknet::ContractAddress;
#[starknet::interface]
pub trait INameRegistry<TContractState> {
fn store_name(ref self: TContractState, name: felt252);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
}
#[starknet::contract]
mod NameRegistry {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: Map<ContractAddress, felt252>,
total_names: u128,
}
#[derive(Drop, Serde, starknet::Store)]
pub struct Person {
address: ContractAddress,
name: felt252,
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.entry(owner.address).write(owner.name);
self.total_names.write(1);
}
// Public functions inside an impl block
#[abi(embed_v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252) {
let caller = get_caller_address();
self._store_name(caller, name);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
self.names.entry(address).read()
}
}
// Standalone public function
#[external(v0)]
fn get_contract_name(self: @ContractState) -> felt252 {
'Name Registry'
}
// 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) {
let total_names = self.total_names.read();
self.names.entry(user).write(name);
self.total_names.write(total_names + 1);
}
}
// Free function
fn get_total_names_storage_address(self: @ContractState) -> felt252 {
self.total_names.__base_address__
}
}
#[cfg(test)]
mod tests;
Similarly to the
constructorfunction, 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 takeselfas 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.
use starknet::ContractAddress;
#[starknet::interface]
pub trait INameRegistry<TContractState> {
fn store_name(ref self: TContractState, name: felt252);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
}
#[starknet::contract]
mod NameRegistry {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: Map<ContractAddress, felt252>,
total_names: u128,
}
#[derive(Drop, Serde, starknet::Store)]
pub struct Person {
address: ContractAddress,
name: felt252,
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.entry(owner.address).write(owner.name);
self.total_names.write(1);
}
// Public functions inside an impl block
#[abi(embed_v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252) {
let caller = get_caller_address();
self._store_name(caller, name);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
self.names.entry(address).read()
}
}
// Standalone public function
#[external(v0)]
fn get_contract_name(self: @ContractState) -> felt252 {
'Name Registry'
}
// 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) {
let total_names = self.total_names.read();
self.names.entry(user).write(name);
self.total_names.write(total_names + 1);
}
}
// Free function
fn get_total_names_storage_address(self: @ContractState) -> felt252 {
self.total_names.__base_address__
}
}
#[cfg(test)]
mod tests;
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.
use starknet::ContractAddress;
#[starknet::interface]
pub trait INameRegistry<TContractState> {
fn store_name(ref self: TContractState, name: felt252);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
}
#[starknet::contract]
mod NameRegistry {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: Map<ContractAddress, felt252>,
total_names: u128,
}
#[derive(Drop, Serde, starknet::Store)]
pub struct Person {
address: ContractAddress,
name: felt252,
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.entry(owner.address).write(owner.name);
self.total_names.write(1);
}
// Public functions inside an impl block
#[abi(embed_v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252) {
let caller = get_caller_address();
self._store_name(caller, name);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
self.names.entry(address).read()
}
}
// Standalone public function
#[external(v0)]
fn get_contract_name(self: @ContractState) -> felt252 {
'Name Registry'
}
// 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) {
let total_names = self.total_names.read();
self.names.entry(user).write(name);
self.total_names.write(total_names + 1);
}
}
// Free function
fn get_total_names_storage_address(self: @ContractState) -> felt252 {
self.total_names.__base_address__
}
}
#[cfg(test)]
mod tests;
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
staticcallopcode 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:
use starknet::ContractAddress;
#[starknet::interface]
pub trait INameRegistry<TContractState> {
fn store_name(ref self: TContractState, name: felt252);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
}
#[starknet::contract]
mod NameRegistry {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: Map<ContractAddress, felt252>,
total_names: u128,
}
#[derive(Drop, Serde, starknet::Store)]
pub struct Person {
address: ContractAddress,
name: felt252,
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.entry(owner.address).write(owner.name);
self.total_names.write(1);
}
// Public functions inside an impl block
#[abi(embed_v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252) {
let caller = get_caller_address();
self._store_name(caller, name);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
self.names.entry(address).read()
}
}
// Standalone public function
#[external(v0)]
fn get_contract_name(self: @ContractState) -> felt252 {
'Name Registry'
}
// 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) {
let total_names = self.total_names.read();
self.names.entry(user).write(name);
self.total_names.write(total_names + 1);
}
}
// Free function
fn get_total_names_storage_address(self: @ContractState) -> felt252 {
self.total_names.__base_address__
}
}
#[cfg(test)]
mod tests;
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.
use starknet::ContractAddress;
#[starknet::interface]
pub trait INameRegistry<TContractState> {
fn store_name(ref self: TContractState, name: felt252);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
}
#[starknet::contract]
mod NameRegistry {
use starknet::storage::{
Map, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: Map<ContractAddress, felt252>,
total_names: u128,
}
#[derive(Drop, Serde, starknet::Store)]
pub struct Person {
address: ContractAddress,
name: felt252,
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.entry(owner.address).write(owner.name);
self.total_names.write(1);
}
// Public functions inside an impl block
#[abi(embed_v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252) {
let caller = get_caller_address();
self._store_name(caller, name);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
self.names.entry(address).read()
}
}
// Standalone public function
#[external(v0)]
fn get_contract_name(self: @ContractState) -> felt252 {
'Name Registry'
}
// 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) {
let total_names = self.total_names.read();
self.names.entry(user).write(name);
self.total_names.write(total_names + 1);
}
}
// Free function
fn get_total_names_storage_address(self: @ContractState) -> felt252 {
self.total_names.__base_address__
}
}
#[cfg(test)]
mod tests;
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: ContractStatein 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.