Ownable

The following Ownable component is a simple component that allows the contract to set an owner and provides an _assert_is_owner function that can be used to ensure that the caller is the owner.

It can also be used to renounce ownership of a contract, meaning that no one will be able to satisfy the _assert_is_owner function.

use starknet::ContractAddress;

#[starknet::interface]
pub trait IOwnable<TContractState> {
    fn owner(self: @TContractState) -> ContractAddress;
    fn transfer_ownership(ref self: TContractState, new: ContractAddress);
    fn renounce_ownership(ref self: TContractState);
}

pub mod Errors {
    pub const UNAUTHORIZED: felt252 = 'Not owner';
    pub const ZERO_ADDRESS_OWNER: felt252 = 'Owner cannot be zero';
    pub const ZERO_ADDRESS_CALLER: felt252 = 'Caller cannot be zero';
}

#[starknet::component]
pub mod ownable_component {
    use starknet::{ContractAddress, get_caller_address};
    use super::Errors;
    use core::num::traits::Zero;

    #[storage]
    struct Storage {
        ownable_owner: ContractAddress,
    }

    #[derive(Drop, starknet::Event)]
    struct OwnershipTransferredEvent {
        previous: ContractAddress,
        new: ContractAddress
    }

    #[derive(Drop, starknet::Event)]
    struct OwnershipRenouncedEvent {
        previous: ContractAddress
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        OwnershipTransferredEvent: OwnershipTransferredEvent,
        OwnershipRenouncedEvent: OwnershipRenouncedEvent
    }

    #[embeddable_as(Ownable)]
    pub impl OwnableImpl<
        TContractState, +HasComponent<TContractState>
    > of super::IOwnable<ComponentState<TContractState>> {
        fn owner(self: @ComponentState<TContractState>) -> ContractAddress {
            self.ownable_owner.read()
        }

        fn transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
            self._assert_only_owner();
            self._transfer_ownership(new);
        }

        fn renounce_ownership(ref self: ComponentState<TContractState>) {
            self._assert_only_owner();
            self._renounce_ownership();
        }
    }

    #[generate_trait]
    pub impl OwnableInternalImpl<
        TContractState, +HasComponent<TContractState>
    > of OwnableInternalTrait<TContractState> {
        fn _assert_only_owner(self: @ComponentState<TContractState>) {
            let caller = get_caller_address();
            assert(caller.is_non_zero(), Errors::ZERO_ADDRESS_CALLER);
            assert(caller == self.ownable_owner.read(), Errors::UNAUTHORIZED);
        }

        fn _init(ref self: ComponentState<TContractState>, owner: ContractAddress) {
            assert(owner.is_non_zero(), Errors::ZERO_ADDRESS_OWNER);
            self.ownable_owner.write(owner);
        }

        fn _transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
            assert(new.is_non_zero(), Errors::ZERO_ADDRESS_OWNER);
            let previous = self.ownable_owner.read();
            self.ownable_owner.write(new);
            self
                .emit(
                    Event::OwnershipTransferredEvent(OwnershipTransferredEvent { previous, new })
                );
        }

        fn _renounce_ownership(ref self: ComponentState<TContractState>) {
            let previous = self.ownable_owner.read();
            self.ownable_owner.write(Zero::zero());
            self.emit(Event::OwnershipRenouncedEvent(OwnershipRenouncedEvent { previous }));
        }
    }
}

A mock contract that uses the Ownable component:

#[starknet::interface]
pub trait IOwned<TContractState> {
    fn do_something(ref self: TContractState);
}

#[starknet::contract]
pub mod OwnedContract {
    use components::ownable::{IOwnable, ownable_component, ownable_component::OwnableInternalTrait};

    component!(path: ownable_component, storage: ownable, event: OwnableEvent);

    #[abi(embed_v0)]
    impl OwnableImpl = ownable_component::Ownable<ContractState>;
    impl OwnableInternalImpl = ownable_component::OwnableInternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        ownable: ownable_component::Storage,
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.ownable._init(starknet::get_caller_address());
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        OwnableEvent: ownable_component::Event,
    }

    #[abi(embed_v0)]
    impl Owned of super::IOwned<ContractState> {
        fn do_something(ref self: ContractState) {
            self.ownable._assert_only_owner();
        // ...
        }
    }
}

#[cfg(test)]
mod tests {
    use core::num::traits::Zero;
    use super::{OwnedContract, IOwnedDispatcher, IOwnedDispatcherTrait};
    use components::ownable::{IOwnable, IOwnableDispatcher, IOwnableDispatcherTrait};

    use starknet::{contract_address_const, ContractAddress};
    use starknet::testing::{set_caller_address, set_contract_address};
    use starknet::storage::StorageMemberAccessTrait;
    use starknet::SyscallResultTrait;
    use starknet::syscalls::deploy_syscall;

    fn deploy() -> (IOwnedDispatcher, IOwnableDispatcher) {
        let (contract_address, _) = deploy_syscall(
            OwnedContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();

        (IOwnedDispatcher { contract_address }, IOwnableDispatcher { contract_address },)
    }

    #[test]
    #[available_gas(2000000)]
    fn test_init() {
        let owner = contract_address_const::<'owner'>();
        set_contract_address(owner);
        let (_, ownable) = deploy();

        assert(ownable.owner() == owner, 'wrong_owner');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_wrong_owner() {
        set_contract_address(contract_address_const::<'owner'>());
        let (_, ownable) = deploy();

        let not_owner = contract_address_const::<'not_owner'>();
        assert(ownable.owner() != not_owner, 'wrong_owner');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_do_something() {
        set_contract_address(contract_address_const::<'owner'>());
        let (contract, _) = deploy();

        contract.do_something();
    // Should not panic
    }

    #[test]
    #[available_gas(2000000)]
    #[should_panic]
    fn test_do_something_not_owner() {
        set_contract_address(contract_address_const::<'owner'>());
        let (contract, _) = deploy();

        set_contract_address(contract_address_const::<'not_owner'>());
        contract.do_something();
    }

    #[test]
    #[available_gas(2000000)]
    fn test_transfer_ownership() {
        set_contract_address(contract_address_const::<'initial'>());
        let (contract, ownable) = deploy();

        let new_owner = contract_address_const::<'new_owner'>();
        ownable.transfer_ownership(new_owner);

        assert(ownable.owner() == new_owner, 'wrong_owner');

        set_contract_address(new_owner);
        contract.do_something();
    }

    #[test]
    #[available_gas(2000000)]
    #[should_panic]
    fn test_transfer_ownership_not_owner() {
        set_contract_address(contract_address_const::<'initial'>());
        let (_, ownable) = deploy();

        set_contract_address(contract_address_const::<'not_owner'>());
        ownable.transfer_ownership(contract_address_const::<'new_owner'>());
    }

    #[test]
    #[available_gas(2000000)]
    #[should_panic]
    fn test_transfer_ownership_zero_error() {
        set_contract_address(contract_address_const::<'initial'>());
        let (_, ownable) = deploy();

        ownable.transfer_ownership(Zero::zero());
    }

    #[test]
    #[available_gas(2000000)]
    fn test_renounce_ownership() {
        set_contract_address(contract_address_const::<'owner'>());
        let (_, ownable) = deploy();

        ownable.renounce_ownership();
        assert(ownable.owner() == Zero::zero(), 'not_zero_owner');
    }

    #[test]
    #[available_gas(2000000)]
    #[should_panic]
    fn test_renounce_ownership_not_owner() {
        set_contract_address(contract_address_const::<'owner'>());
        let (_, ownable) = deploy();

        set_contract_address(contract_address_const::<'not_owner'>());
        ownable.renounce_ownership();
    }

    #[test]
    #[available_gas(2000000)]
    #[should_panic]
    fn test_renounce_ownership_previous_owner() {
        set_contract_address(contract_address_const::<'owner'>());
        let (contract, ownable) = deploy();

        ownable.renounce_ownership();

        contract.do_something();
    }
}
Last change: 2024-06-05, commit: c0ab62f