Visibility and Mutability
Visibility
There are two types of functions in Starknet contracts:
- Functions that are accessible externally and can be called by anyone.
- Functions that are only accessible internally and can only be called by other functions in the contract.
These functions are also typically divided into two different implementation blocks. The first impl
block for externally accessible functions is explicitly annotated with an #[abi(embed_v0)]
attribute. This indicates that all the functions inside this block can be called either as a transaction or as a view function. The second impl
block for internally accessible functions is not annotated with any attribute, which means that all the functions inside this block are private by default.
State Mutability
Regardless of whether a function is internal or external, it can either modify the contract's state or not. When we declare functions that interact with storage variables inside a smart contract,
we need to explicitly state that we are accessing the ContractState
by adding it as the first parameter of the function. This can be done in two different ways:
- If we want our function to be able to mutate the state of the contract, we pass it by reference like this:
ref self: ContractState
- If we want our function to be read-only and not mutate the state of the contract, we pass it by snapshot like this:
self: @ContractState
Read-only functions, also called view functions, can be directly called without making a transaction. You can interact with them directly through an RPC node to read the contract's state, and they're free to call! External functions, that modify the contract's state, on the other hand, can only be called by making a transaction.
Internal functions can't be called externally, but the same principle applies regarding state mutability.
Let's take a look at a simple example contract to see these in action:
#[starknet::interface]
pub trait IExampleContract<TContractState> {
fn set(ref self: TContractState, value: u32);
fn get(self: @TContractState) -> u32;
}
#[starknet::contract]
pub mod ExampleContract {
#[storage]
struct Storage {
value: u32
}
// The `#[abi(embed_v0)]` attribute indicates that all
// the functions in this implementation can be called externally.
// Omitting this attribute would make all the functions internal.
#[abi(embed_v0)]
impl ExampleContract of super::IExampleContract<ContractState> {
// The `set` function can be called externally
// because it is written inside an implementation marked as `#[abi(embed_v0)]`.
// It can modify the contract's state as it is passed as a reference.
fn set(ref self: ContractState, value: u32) {
self.value.write(value);
}
// The `get` function can be called externally
// because it is written inside an implementation marked as `#[abi(embed_v0)]`.
// However, it can't modify the contract's state, as it is passed as a snapshot
// -> It's only a "view" function.
fn get(self: @ContractState) -> u32 {
// We can call an internal function from any functions within the contract
PrivateFunctionsTrait::_read_value(self)
}
}
// The lack of the `#[abi(embed_v0)]` attribute indicates that all the functions in
// this implementation can only be called internally.
// We name the trait `PrivateFunctionsTrait` to indicate that it is an
// internal trait allowing us to call internal functions.
#[generate_trait]
pub impl PrivateFunctions of PrivateFunctionsTrait {
// The `_read_value` function is outside the implementation that is
// marked as `#[abi(embed_v0)]`, so it's an _internal_ function
// and can only be called from within the contract.
// However, it can't modify the contract's state, as it is passed
// as a snapshot: it is only a "view" function.
fn _read_value(self: @ContractState) -> u32 {
self.value.read()
}
}
}
#[cfg(test)]
mod test {
use super::{ExampleContract, IExampleContractDispatcher, IExampleContractDispatcherTrait};
use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};
// These imports will allow us to directly access and set the contract state:
// - for `value` storage variable access
use super::ExampleContract::valueContractMemberStateTrait;
// - for `PrivateFunctionsTrait` internal functions access
// implementation need to be public to be able to access it
use super::ExampleContract::PrivateFunctionsTrait;
// to set the contract address for the state
// and also be able to use the dispatcher on the same contract
use starknet::testing::set_contract_address;
#[test]
fn can_call_set_and_get() {
let (contract_address, _) = deploy_syscall(
ExampleContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
)
.unwrap_syscall();
// You can interact with the external entrypoints of the contract using the dispatcher.
let contract = IExampleContractDispatcher { contract_address };
// But for internal functions, you need to use the contract state.
let mut state = ExampleContract::contract_state_for_testing();
set_contract_address(contract_address);
// The contract dispatcher and state refer to the same contract.
assert_eq!(contract.get(), state.value.read());
// We can set from the dispatcher
contract.set(42);
assert_eq!(contract.get(), state.value.read());
assert_eq!(42, state.value.read());
assert_eq!(42, contract.get());
// Or directly from the state for more complex operations
state.value.write(24);
assert_eq!(contract.get(), state.value.read());
assert_eq!(24, state.value.read());
assert_eq!(24, contract.get());
// We can also acces internal functions from the state
assert_eq!(state._read_value(), state.value.read());
assert_eq!(state._read_value(), contract.get());
}
}