Getting Started with Cairo: Upgradable Contracts and Cross-Chain Messaging

·

Overview

For developers familiar with my previous articles, you'll notice I've followed a similar learning path—starting with ERC20 tokens and progressing to upgradable contracts. This article explores Cairo's approach to upgradable contract programming.

Key differences from Solidity:

We'll build upon Cairo 2 Practical Guide: Writing, Testing, and Deploying ERC-20 Tokens to create a native cross-chain ERC20 token supporting:

  1. transfer_to_L2 function (Ethereum → StarkNet)
  2. transferToL1 function (StarkNet → Ethereum)

👉 Complete code repository

Upgradable Contracts in Cairo

Class Hash Architecture

StarkNet's fundamental separation between:

This provides native separation of logic and state—similar to logic contracts vs. proxy contracts in Solidity.

Upgrade Mechanism

use starknet::syscalls::replace_class_syscall;

fn upgrade(self: @ContractState, new_class_hash: ClassHash) {
    replace_class_syscall(new_class_hash);
}

Key advantages over Solidity:

  1. No storage slot conflicts (storage uses variable name hashes)
  2. Direct class replacement without migration
  3. No need for complex proxy patterns
⚠️ Projects using upgradable contracts on StarkNet may carry higher governance risks.

Cross-Chain Communication

L2 → L1 Messaging

Workflow

  1. Cairo contract calls send_message_to_l1_syscall
  2. StarkNet node computes message hash:

    keccak256(abi.encodePacked(
        FromAddress, 
        ToAddress, 
        Payload.length, 
        Payload
    ));
  3. Core contract emits LogMessageToL1 event
  4. L1 contract consumes message via consumeMessageFromL2

Implementation Example

let mut message_payload = array![
    l1_recipient.into(), 
    amount.low.into(), 
    amount.high.into()
];
send_message_to_l1_syscall(
    to_address: self.l1_token.read(), 
    payload: message_payload.span()
);

Gas Costs: ~20k gas for L1 state write (automatically handled by StarkNet gas estimation)

L1 → L2 Messaging

Workflow

  1. L1 calls sendMessageToL2 on core contract
  2. StarkNet node detects event and triggers L2 contract
  3. Only functions with #[l1_handler] annotation are callable

Implementation Example

#[l1_handler]
fn despoit_from_L1(from_address: felt252, account: ContractAddress, amount: u256) {
    assert(from_address == l1_token::read(), 'UNAUTHORIZED_L1_CALL');
    _mint(account, amount);
}

Error Handling:

Contract Implementation

L2 Cairo Contract

Key additions to standard ERC20:

  1. Burn Function:

    fn burn(ref self: ContractState, amount: u256) {
     let sender = get_caller_address();
     self._total_supply.write(self._total_supply.read() - amount);
     self._balances.write(sender, self._balances.read(sender) - amount);
    }
  2. Cross-chain Transfer:

    fn transfer_to_L1(ref self: ContractState, l1_recipient: EthAddress, amount: u256) {
     self.burn(amount);
     send_message_to_l1_syscall(...);
    }

L1 Solidity Contract

Using Solmate ERC20 as base:

function despoitFromL2(uint256 fromAddress, uint256[] calldata payload) external {
    IStarknetMessaging(starkNetAddress).consumeMessageFromL2(fromAddress, payload);
    (address receiver, uint256 amount) = parsePayload(payload);
    _mint(receiver, amount);
}

Testing Cross-Chain Transfers

L2 → L1 Test Flow

  1. Call transfer_to_L1 on Cairo contract
  2. Monitor message hash on StarkScan
  3. After 4-8 hours, verify message appears in core contract
  4. Consume message on L1

L1 → L2 Test Flow

cast send $bridge "transferToL2(uint256,uint256)" $L2ADDRESS 0.5ether \
    --value 0.003ether \
    --rpc-url $RPC_URL \
    --private-key $PRIVATE_KEY

👉 View transaction example

FAQ

Why doesn't Cairo need proxy contracts?

StarkNet's architecture inherently separates logic (class) from state (contract), eliminating the need for proxy patterns used in Solidity.

How long do cross-chain messages take?

What happens if L1→L2 transaction fails?

After 5 days, users can:

  1. Call startL1ToL2MessageCancellation
  2. Wait 5 days
  3. Call cancelL1ToL2Message to recover assets

How to estimate L2 gas costs?

Use Starkli's estimation:

starkli invoke --estimate-only 0x0437...1e81e3 mint u256:100

Conclusion

Key takeaways:

  1. Simplified Upgrades: Cairo's class/contract separation enables easier upgrades than Solidity
  2. Native Cross-Chain: StarkNet's architecture provides built-in messaging primitives
  3. New Patterns: Developers must adapt to StarkNet's unique storage and upgrade patterns

This implementation demonstrates how to build truly chain-agnostic tokens using StarkNet's cross-chain capabilities. Future improvements could include: