Ownable
The following Ownable
component is a simple component that allows the contract to set an owner and provides a _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);
}
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)]
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_zero(), Errors::ZERO_ADDRESS_CALLER);
assert(caller == self.ownable_owner.read(), Errors::UNAUTHORIZED);
}
fn _init(ref self: ComponentState<TContractState>, owner: ContractAddress) {
assert(!owner.is_zero(), Errors::ZERO_ADDRESS_OWNER);
self.ownable_owner.write(owner);
}
fn _transfer_ownership(ref self: ComponentState<TContractState>, new: ContractAddress) {
assert(!new.is_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();
}
}