This is a study note of Hello, StarkNet. StarkNet is a permissionless decentralized Validity-Rollup (ZK-Rollup) based on STARK, a scalable cryptographic proof system.

1 StarkNet Contract

The first line should be %lang starknet to declare that this is a StarkNet contract file.

There is no need to use the %builtins directive in StarkNet contracts.

A contract can have storage variables to store its states. The @storage_var decorator declares a storage variable that is part of the storage. All storage cells are initialized to zero and can be accessed using variable.read() and variable.write() functions. A storage variable requires three implicit arguments: pedersen_ptr, range_check_ptr and syscall_ptr. The syscall_ptr is unique to StartNet contracts.

A contract has no main() function. Functions annotated as @external or @view can be called by the users or other contracts of Starknet.

Because the contract’s auther, the user involing the function, and the operator running it are likely to be different entities, the contract cannot use hints. For efficiency reason, hints are still used by some standard library functions that are whitelisted and are available to the contract.

2 CLI

To compile a contract, use the command starknet-compile contract.cairo --output contract_compiled.json --abi contract_abi.json. The contract ABI file contains a list of all the callable functions and their inputs and outputs.

To delpoy it, set a STARTNET_NETWORK environment variable or specify the --network=alpha-goerli and run the command starknet deploy --contract contract_compiled.json. The deploy command will return the contract address and the transaction hash.

To call a contract function that change its states, use starknet invok --address ${CONTRACT_ADDRESS} --abi contract_abi.json --function func_name --inputs 1234.

Use starknet tx_status --hash ${TRANSACTION_HASH} to query transaction status.

Use starknet call --address ${CONTRACT_ADDRESS} --abi contract_abi.json --function func_name --inputs 1234 to call a read-only funciton. Using call instead of invoke on a function that may change the state will return the result of the funciton without actually applying it - a dry run.

To get transaction information such as block_hash, block_number, transaction_index, transaction details and transaction_failure_reason, use starknet get_transaction --hash TRANSACTION_HASH.

To get transaction receipt containing execution information, such as L1<->L2 interaction and consumed resources, use starknet get_transaction_receipt --hash TRANSACTION_HASH.

Once the deployed transaction is accepted on-chain, you can get the ABI and code using starknet get_code --contract_address ${CONTRACT_ADDRESS}. To get the full contract definition, use starknet get_full_contract --contract_address ${CONTRACT_ADDRESS}.

To get the block info, use starknet get_block --number BLOCK_NUMBER. Without --number, it returns the last block. Use --hash to query by hash.

Each storage has a key that can be retrieved using the following code:

1
2
3
4
from starkware.starknet.public.abi import get_storage_var_address

balance_key = get_storage_var_address('balance')
print(f'Balance key: {balance_key}')

Then one can find the storage value using starknet get_storage_at --contract_address CONTRACT_ADDRESS --key KEY_VALUE.

Some CLI functions have an additional argument --block_hash to specify a specific block. Otherwise, the query will be applied to the last block.

3 Adding User Authentication

The @storage_var decorator allows you to add multiple arguments to create a storage map. A common one is to add a user : felt argument that creates a storage variable for each user. The corresponding read and write also have the user : felt argument.

Use let (user) = get_caller_address() to get the address of the source contract, either an account contract or another contract, that called this contract.

To get detail error message, use starknet tx_status --hash TX_HASH --contracts ${CONTRACT_ADDRESS}:contract_compiled.json --error_message.

4 Constructors

A contract may need to initialize its state before it is ready for public use. The contract constructor is defined using the @constructor decorator and its name must be constructor. The constructor semantics are similar to that of any other external function, except that the constructor is guaranteed to run during the contract deployment and it cannot be invoked again after the contract is deployed.

When you deploy the contract, pass the constructor arguments using the --inputs argument.

5 More Features

A storage variable can also be a tuple or a structure in its argument and stored value.

The structure or tuple shouldn’t contain pointers, the so-called felt-only types.

To pass an array, use two consecutive arguments: a_len : felt and a : T*.

You can retrieve the transaction information (which includes, for example, the signature and the transaction fee), by using the get_tx_info() library function. It returns a TxInfo struct.

You can get the current block number and timestamp (seconds since unix epoch) by using the get_block_number() and get_block_timestamp() library functions.

6 Calling Another Contract

A contract function may invoke an external function of another contract. In order to do it, you need to define an interface by copying the declarations of the external functions using @contract_interface and namespace ISomeContract. Then you can call the functions as ISomeContract.func_name(contract_address=contract_address, arg1 = arg1). The first argument is the callee contract address. In addition, the syscall_ptr and the range_check_ptr implicit arguments are required.

You can get the current contract’s address by using the get_contract_address() library function.

A delegate call is a way to invoke a function declared in another contract within the context of the calling contract. You call the function with a delegate_ prefix like ISomeContract.delegate_func_name.

Similarly, get_caller_address() and get_contract_address() will return the same value if they were called from the delegated calling function.

7 Events

You can define an event using the @event decorator. Then emit the event using some_func.emit(args).

The event contains the following fields:

  • from_address: the address of the contract emitting the event.
  • data: the event arguments.
  • keys: The event’s key is derived from the name of the event. You can use Python function to get the even key from its name: get_selector_from_name('func_name').

Currently StarkNet doesn’t have API to fetch events from a given contract.

8 Interacting with L1 Contracts

StarkNet contract can send and receive messages to/from an L1 contract. You should design the message protocol between an L2 contract and its L1 conterpart.

For messages from L2 to L1, the L2 call the library function send_message_to_l1(to, payload). Once the L2 transaction is accepted on-chain, the message is stored on the L1 StarkNet core contract. The L1 contract specified by the to invokes the consumeMessageFromL2().

For message from L1 to L2, the L1 contract calls send_message(selector) and the StarkNet Sequencer consumes the message and invokes the requested L2 function.

9 Default Entry Point

There are cases where the contract entry points are not known in advance. The most prominent example is a delegate proxy that forwards calls to an implementation contract. Such a proxy can be implemented using the __default__ entry point.

The __l1_default__ entry point that is executed when an L1 handler is invoked but the requested selector is missing.

10 AMM

The Automated Market Making (AMM) shows how developers specify the verifiable business logic and constraints while enjoying massive scalability without compromising security. The developers only needs to focus on the invocable functions and the relevant storage variables used to maintain the state of the application.