# Contract syntax update: Introducing components TLDR; Components are a new way to write composable contracts. It is now easier to separate storage and logic per concern as opposed to having everything defined in one place. It makes writing contracts easier and reduces potential bugs by preventing code duplication. ## Current way of performing composability - Every contract has it's own state - You can use the state of a contract B inside a contract A by using `let state = Admin::unsafe_new_contract_state()` - You can then call ContractB's functions in the context of ContractA by passing its state explicitly to its functions, e.g. `Admin::assert_only_admin(@state);` Problems - Heavy syntax - Needs to manipulate multiple contract states inside a single contract - No difference between contracts that are meant to be deployed and those who are just used to separate logic and storage by concern - "Unsafe" -> Eventual storage collisions ## Solutions: Components Components can be plugged inside a contract, which will then be able to inherit both the logic and the storage of the component - ContractState ==> ComponentState. Each Component has it's own Storage struct and Events. - Component logic can be "embedded" inside contracts. - Component storage can be "nested" inside contracts. - Components compile independently but cant be deployed independently Example: An Ownable Component contains a Storage struct that stores an `owner`. It's logic contains functions to set the current owner, retrieve it, and check whether the current caller is the owner. ```rust #[starknet::component] mod ownable { use starknet::ContractAddress; #[storage] struct Storage { owner: starknet::ContractAddress, } #[embeddable_as(Ownable)] #[generate_trait] impl OwnableImpl<TContractState, impl X: HasComponent<TContractState>> of TransferTrait<TContractState, X> { fn init_ownable(ref self: ComponentState<TContractState>, owner: ContractAddress) { self.owner.write(owner); } #[external(v0)] fn owner(self: @ComponentState<TContractState>) -> ContractAddress { self.owner.read() } #[external(v0)] fn transfer_ownership(ref self: ComponentState<TContractState>, new_owner: ContractAddress) { self.validate_ownership(); self.owner.write(new_owner); } fn validate_ownership(self: @ComponentState<TContractState>) { assert(self.owner.read() == starknet::get_caller_address(), 'Wrong owner.'); } } } ``` - Components are marked as `#[starknet::component]` - Reuses the Trait and Impl system from Cairo - You can define functions that will be "embeddable" inside a contract using an Impl marked as `#[embeddable_as(NAME)]`, which will allow importing the logic of the component inside your contract - Marking functions as "#[external(v0)]" makes them a part of the contract ABI when embedded into a contract - The implementation is generic over `TContractState`, so that it can be used in multiple contracts. The following code is generated from the component impl: ```rust #[starknet::embeddable] impl Transfer< TContractState, impl X: HasComponent<TContractState>, impl TContractStateDrop: Drop<TContractState> > of TransferImplTrait<TContractState> { fn init_ownable(ref self: TContractState, owner: ContractAddress) { let mut component = self.get_component_mut(); component.init_ownable(owner, ) } #[external(v0)] fn owner(self: @TContractState) -> ContractAddress { let component = self.get_component(); component.owner() } #[external(v0)] fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress) { let mut component = self.get_component_mut(); component.transfer_ownership(new_owner, ) } fn validate_ownership(self: @TContractState) { let component = self.get_component(); component.validate_ownership() } } ``` - Calls the function of the component directly. This is possible because of the `X: HasComponent<TContractState` restriction that ensure the contract embeds this component - This is the code that will be embedded in the contract. See the `#[starknet::embeddable]` attribute, an the name of the Impl is different from the one defined in the component. ### Using components in contracts The following contract uses the `Ownable` component to handle all the logic related contract owner. ```rust #[starknet::contract] mod my_contract { use starknet::ContractAddress; #[storage] struct Storage { #[nested(v0)] ownable: super::ownable::Storage, balance: u128, } #[event] #[derive(Drop, starknet::Event)] enum Event { Ownable: super::ownable::Event, } component!(path: super::ownable, storage: ownable, event: Ownable); #[embed(v0)] impl OwnershipTransfer = super::ownable::Transfer<ContractState>; #[embed(v0)] #[generate_trait] impl Impl of Trait { #[constructor] fn constructor(ref self: ContractState, owner: ContractAddress, initial: u128) { self.init_ownable(owner); self.balance.write(initial); } #[external(v0)] fn get_balance(self: @ContractState) -> u128 { self.balance.read() } #[external(v0)] fn set_balance(ref self: ContractState, new_balance: u128) { self.validate_ownership(); self.balance.write(new_balance); } } } ``` - The ownable component needs to be explicitly brought in the contract using the `component!` macro. - You will need to specify the path to the component, the storage name for the ownable component and the event enum name for the ownable component, and use these names in the Storage struct and the Event enum. - To import the *logic* of the component, you need to declare its implementation in the contract by assigning it the name of the `#[embeddable_as(NAME)]` impl in the component - `#[embed(v0)]` is a new attribute that is similar to `#[external(v0)]`, but can include constructor functions and L1 handlers - Once component logic is embedded into the contract, you can call its function with `self.fn_name()` - You can directly access a component's storage with `self.component_storage_name.field` ### Using components inside Kakarot I'm not 100% sure how we will be able to use components inside Kakarot yet, but here are some ideas: - The `SSTORE` and `SSLOAD` opcodes could be implemented as a `KakarotStorage` component. - This component would not have it's own Storage struct, because we will not write content to slots with a precomputed address - Instead, we will use the Impl system to restrict our component to implement the `KakarotContract` trait. Implementing this trait would mean that the parent of our component is a contract of "type" `KakarotContract`, meaning that we can access its functions inside the component - We will define a `write_storage` and `read_storage` function in the KakarotContract. When executing Storage opcodes, we will do something like ```rust let kakarot = self.get_contract(); kakarot.write_storage(address,value); ``` Which will allow us to write to Kakarot's storage from a component. # Question and Feedback from the presentation - `#[external(v0)]` could be renamed `#[entrypoint(v0)]` to clear the confusion between "view/external" - What is the reason behind the three arguments in the component! macro? Is it to avoid nameclashes? - Having the possibility of using multiple times the same component in a contract would be great. For example, that would allow us of using `ownable` two times to handle multiple access restrictions. It's not possible for now because of clashes in impl names. - What happens if one forgets to import the storage used internally by a given method of the component? - would read_storage and write_storage syscalls work the same? - It seems like we can actually use `storage_read_syscall` inside components to access a contract's storage slot by it's address. Is that intended? Will it read the storage from the contract the component is embedded in?