Interfaces and Dispatchers

This section describes the interfaces OpenZeppelin Contracts for Cairo offer, and explains the design choices behind them.

Interfaces can be found in the module tree under the interface submodule, such as token::erc20::interface. For example:

use openzeppelin::token::erc20::interface::IERC20;

or

use openzeppelin::token::erc20::dual20::DualCaseERC20;
For simplicity, we’ll use ERC20 as example but the same concepts apply to other modules.

Interface traits

The library offers three types of traits to implement or interact with contracts:

Standard traits

These are associated with a predefined interface such as a standard. This includes only the functions defined in the interface, and is the standard way to interact with a compliant contract.

#[starknet::interface]
trait IERC20<TState> {
    fn name(self: @TState) -> ByteArray;
    fn symbol(self: @TState) -> ByteArray;
    fn decimals(self: @TState) -> u8;
    fn total_supply(self: @TState) -> u256;
    fn balance_of(self: @TState, account: ContractAddress) -> u256;
    fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256;
    fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool;
    fn transfer_from(
        ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
    fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool;
}

ABI traits

They describe a contract’s complete interface. This is useful to interface with a preset contract offered by this library, such as the ERC20 preset that includes functions from different traits such as IERC20 and IERC20Camel.

#[starknet::interface]
trait ERC20ABI<TState> {
    fn name(self: @TState) -> ByteArray;
    fn symbol(self: @TState) -> ByteArray;
    fn decimals(self: @TState) -> u8;
    fn total_supply(self: @TState) -> u256;
    fn balance_of(self: @TState, account: ContractAddress) -> u256;
    fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256;
    fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool;
    fn transfer_from(
        ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
    fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool;
}

Dispatcher traits

This is a utility trait to interface with contracts whose interface is unknown. Read more in the DualCase Dispatchers section.

#[derive(Copy, Drop)]
struct DualCaseERC20 {
    contract_address: ContractAddress
}

trait DualCaseERC20Trait {
    fn name(self: @DualCaseERC20) -> ByteArray;
    fn symbol(self: @DualCaseERC20) -> ByteArray;
    fn decimals(self: @DualCaseERC20) -> u8;
    fn total_supply(self: @DualCaseERC20) -> u256;
    fn balance_of(self: @DualCaseERC20, account: ContractAddress) -> u256;
    fn allowance(self: @DualCaseERC20, owner: ContractAddress, spender: ContractAddress) -> u256;
    fn transfer(self: @DualCaseERC20, recipient: ContractAddress, amount: u256) -> bool;
    fn transfer_from(
        self: @DualCaseERC20, sender: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
    fn approve(self: @DualCaseERC20, spender: ContractAddress, amount: u256) -> bool;
}

Dual interfaces

Following the Great Interface Migration plan, we added snake_case functions to all of our preexisting camelCase contracts with the goal of eventually dropping support for the latter.

In short, we offer two types of interfaces and utilities to handle them:

  1. camelCase interfaces, which are the ones we’ve been using so far.

  2. snake_case interfaces, which are the ones we’re migrating to.

This means that currently most of our contracts implement dual interfaces. For example, the ERC20 preset contract exposes transferFrom, transfer_from, balanceOf, balance_of, etc.

Dual interfaces are available for all external functions present in previous versions of OpenZeppelin Contracts for Cairo (v0.6.1 and below).

IERC20

The default version of the ERC20 interface trait exposes snake_case functions:

#[starknet::interface]
trait IERC20<TState> {
    fn name(self: @TState) -> ByteArray;
    fn symbol(self: @TState) -> ByteArray;
    fn decimals(self: @TState) -> u8;
    fn total_supply(self: @TState) -> u256;
    fn balance_of(self: @TState, account: ContractAddress) -> u256;
    fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256;
    fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool;
    fn transfer_from(
        ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
    fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool;
}

IERC20Camel

On top of that, we also offer a camelCase version of the same interface:

#[starknet::interface]
trait IERC20Camel<TState> {
    fn name(self: @TState) -> ByteArray;
    fn symbol(self: @TState) -> ByteArray;
    fn decimals(self: @TState) -> u8;
    fn totalSupply(self: @TState) -> u256;
    fn balanceOf(self: @TState, account: ContractAddress) -> u256;
    fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256;
    fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool;
    fn transferFrom(
        ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256
    ) -> bool;
    fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool;
}

DualCase dispatchers

DualCase dispatchers won’t work on live chains (mainnet or testnets) until they implement panic handling in their runtime. Dispatchers work fine in testing environments.

In order to ease this transition, OpenZeppelin Contracts for Cairo offer what we call DualCase dispatchers such as DualCaseERC721 or DualCaseAccount.

These modules wrap a target contract with a compatibility layer to expose a snake_case interface no matter what casing the underlying contract uses. This way, an AMM wouldn’t have problems integrating tokens independently of their interface.

For example:

let token = DualCaseERC20 { contract_address: target };
token.transfer_from(OWNER(), RECIPIENT(), VALUE);

This is done by simply executing the snake_case version of the function (e.g. transfer_from) and falling back to the camelCase one (e.g. transferFrom) in case it reverts with ENTRYPOINT_NOT_FOUND, like this:

fn try_selector_with_fallback(
    target: ContractAddress, selector: felt252, fallback: felt252, args: Span<felt252>
) -> SyscallResult<Span<felt252>> {
    match call_contract_syscall(target, selector, args) {
        Result::Ok(ret) => Result::Ok(ret),
        Result::Err(errors) => {
            if *errors.at(0) == 'ENTRYPOINT_NOT_FOUND' {
                return call_contract_syscall(target, fallback, args);
            } else {
                Result::Err(errors)
            }
        }
    }
}

Trying the snake_case interface first renders camelCase calls a bit more expensive since a failed snake_case call will always happen before. This is a design choice to incentivize casing adoption/transition as per the Great Interface Migration.