Migrating a contract from Cairo v1 to Cairo v2

With the v2.0.0 release of the Cairo compiler, the Starknet contract syntax has evolved, affecting the organization of functions, storage, and events.

For more information on the latest syntax changes, see the Community Forum post Cairo 1: Contract Syntax is Evolving.

Prerequisites
  • A contract written with the Cairo compiler v1

  • The most recent version of the Cairo compiler

Procedure
  1. Change the contract annotation from #[contract] to #[starknet::contract]. For example::

    • Cairo v1

    • Cairo v2

    #[contract]
    mod CounterContract {
       ...
    }
    #[starknet::contract]
    mod CounterContract {
       ...
    }
  2. Annotate the Storage struct with the #[storage] attribute. For example:

    • Cairo v1

    • Cairo v2

    struct Storage {
        counter: u128,
        other_contract: IOtherContractDispatcher
    }
    #[storage]
    struct Storage {
        counter: u128,
        other_contract: IOtherContractDispatcher
    }
  3. Gather your contract’s external and view function signatures under a trait annotated with #[starknet::interface] as follows:

    • Add a generic parameter to the trait. In the following example, the name TContractState represents the state of your contract.

    • For view functions, add the self: @TContractState argument.

    • For external functions, add the ref self: TContractState argument.

    • Static functions that do not touch storage or emit events do not require an additional argument.

      For example:

      • Cairo v1

      • Cairo v2

      #[contract]
      mod CounterContract {
         #[external]
         fn increase_counter(amount: u128) { ... }
         #[external]
         fn decrease_counter(amount: u128) { ... }
         #[view]
         fn get_counter() -> u128 { ... }
      }
      #[starknet::interface]
      trait ICounterContract<TContractState> {
         fn increase_counter(ref self: TContractState, amount: u128);
         fn decrease_counter(ref self: TContractState, amount: u128);
         fn get_counter(self: @TContractState) -> u128;
      }
      #[starknet::contract]
      mod CounterContract {
         ...
      }
  4. Add the external and view function bodies under an impl of the interface trait, and mark the impl with the [external(v0)] attribute, which generates the type of dispatcher that is used to call the contract.

    For example:

    • Cairo v1

    • Cairo v2

    #[contract]
    mod CounterContract {
       #[external]
       fn increase_counter(amount: u128) { ... }
       #[external]
       fn decrease_counter(amount: u128) { ... }
       #[view]
       fn get_counter() -> u128 { ... }
    }
    #[starknet::interface]
    trait ICounterContract<TContractState> {
       fn increase_counter(ref self: TContractState, amount: u128);
       fn decrease_counter(ref self: TContractState, amount: u128);
       fn get_counter(self: @TContractState) -> u128;
    }
    #[starknet::contract]
    mod CounterContract {
       #[external(v0)]
       impl CounterContract of super::ICounterContract<ContractState> {
          fn increase_counter(ref self: ContractState, amount: u128) { ... }
          fn decrease_counter(ref self: ContractState, amount: u128) { ... }
          fn get_counter(self: @ContractState) -> u128 { ... }
       }
    }
  5. Replace the #[abi] attribute with #[starknet::interface].

    While it doesn’t affect the generated code, adding to the trait a generic parameter T representing the contract’s state, and adding the ref self: T argument to external functions and self: @T argument for view functions makes the implementation more complete.

    For example:

    • Cairo v1

    • Cairo v2

    #[abi]
    trait IOtherContract {
        fn decrease_allowed() -> bool;
    }
    #[starknet::interface]
    trait IOtherContract<TContractState> {
        fn decrease_allowed(self: @TContractState) -> bool;
    }
  6. Modify storage accesses to happen through ContractState or @ContractState.

    No external functions in the contract that access storage also need to get it as an argument.

    For example:

    • Cairo v1

    • Cairo v2

    let current = counter::read();
    let current = self.counter.read();
  7. Unify all the contract’s events under the Event enum, and add a corresponding struct for every variant.

    All the structs must derive the Event trait, and each member type must implement the Serde trait.

    For example:

    • Cairo v1

    • Cairo v2

    #[event]
    fn counter_increased(amount: u128) {}
    #[event]
    fn counter_decreased(amount: u128) {}
    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        CounterIncreased: CounterIncreased,
        CounterDecreased: CounterDecreased
    }
    #[derive(Drop, starknet::Event)]
    struct CounterIncreased {
        amount: u128
    }
    #[derive(Drop, starknet::Event)]
    struct CounterDecreased {
        amount: u128
    }
  8. Emit events via the ContractState type. For example:

    • Cairo v1

    • Cairo v2

    fn increase_counter(amount: u128) {
        ...
        counter_increased(amount);
    }
    fn increase_counter(ref self: ContractState, amount: u128) {
        ...
        self.emit(Event::CounterIncreased(CounterIncreased { amount }));
    }