Contract syntax #2058
Replies: 12 comments 50 replies
-
trait Event<T> {
fn get_keys(self: T) -> Array::<felt>;
fn get_values(self: T) -> Array::<felt>;
fn from_keys_and_values(keys: Array::<felt>, values: Array::<felt>) -> T;
}
trait Event<T> {
fn keys(self: T) -> @Array::<felt>;
fn values(self: T) -> @Array::<felt>;
fn from_keys_and_values(keys: Array::<felt>, values: Array::<felt>) -> T;
} I have also used hypothetical snapshot syntax, because I feel like keys/values should be read-only? |
Beta Was this translation helpful? Give feedback.
-
I know this is not there yet, but I welcome treating procmacros as regular items in resolution scope, and would love them to be provided by a module/package: #[starknet::storage]
struct MyContract {
balances: Mapping<address, u128>
} or use starknet::storage;
#[storage]
struct MyContract {
balances: Mapping<address, u128>
} |
Beta Was this translation helpful? Give feedback.
-
#[storage]
struct MyOtherContract {
inner: MyContract,
} Hmm, this seems to be conflicting with storage variables. I don't know how plugins API look like, but without access to type system (which forms a plugin<>types cycle!!, because type system needs plugins output in order to work), you won't be able to reliably differentiate storage vars from inner contracts... maybe put it like this? #[starknet::contract]
struct MyOtherContract {
#[storage]
myvar: felt,
#[storage]
myvar2: felt,
#[extends]
inner: MyContract,
} |
Beta Was this translation helpful? Give feedback.
-
I was wondering what are your thoughts on this suggestion vs the syntax we have today (annotated module) |
Beta Was this translation helpful? Give feedback.
-
I find proposed My proposal: trait Contract<T> {
type ConstructorArgs;
fn construct(args: ConstructorArgs) -> Result<T>; // perhaps panic?
} |
Beta Was this translation helpful? Give feedback.
-
How intra contract calls will look like? Knowing storage type of the contract being called should not be necessary. |
Beta Was this translation helpful? Give feedback.
-
For events more compact syntax should be available. I mean instead of:
it should be possible to:
|
Beta Was this translation helpful? Give feedback.
-
Can you clarify the exact meaning of #[starknet::storage]
struct MyOtherContract {
#[compose]
inner: MyContract,
} |
Beta Was this translation helpful? Give feedback.
-
Is there any proposal/example on how extensibility should work when there's multiple modules being composed down the line? Say you want to have an account contract that extends an
|
Beta Was this translation helpful? Give feedback.
-
I'd be careful with this, since ABI compatibility does not guarantee equal behavior. As an example edge case, a contract may maintain the ABI but then revert for every call to a given function. |
Beta Was this translation helpful? Give feedback.
-
Here is what I think your ////////////////////
// LinkToContract.cairo
#[storage]
struct ToContractStorage {
address: ContractAddress;
};
#[abi]
trait LinkToContract<Storage> {
#[view]
get(self: Storage) -> ContractAddress;
#[external]
set(self: Storage, address: ContractAddress);
}
// Generic implementation for a given struct T.
// The second argument is a 'derivation path' to obtain a ToContractStorage-compatible struct from T. My imaginary syntax to call this is `self.get_subcomponent<A>()`
impl LinkToContract<T, impl A: DeriveComponent<T, ToContractStorage>> of LinkToContract<T> {
fn get(self: T, ) -> ContractAddress { self.get_subcomponent<A>().address::read() }
fn set(self: T, address: ContractAddress) { self.get_subcomponent<A>().address::write(address) }
}
////////////////////
// ERC20.cairo
#[storage]
struct Erc20Storage<ValueType> {
balance: Mapping<ContractAddress, ValueType>
}
#[abi]
trait ERC20<Contract, ExternalValueType> {
#[view]
fn balanceOf(self: Contract, owner: ContractAddress) -> ExternalValueType;
#[external]
fn transferFrom(self: Contract, from: ContractAddress, to: ContractAddress, value: ExternalValueType);
}
// Again this 'derivation path' this time parametrised.
impl ERC20<Contract, ExternalValueType, InternalValueType, impl A: DeriveComponent<Contract, Erc20Storage<InternalValueType>> of ERC20<Contract, ExternalValueType>
{
fn balanceOf(self: Contract, owner: ContractAddress) -> ExternalValueType {
// Because we get an InternalValueType we need to convert it appropriately via `into`
self.get_subcomponent<A>().balance::read(owner).into()
}
...
}
////////////////////
// MyContractClass.cairo
#[contract_class]
mod MyContract {
#[storage]
struct MyStorage {
#[component] // TBH not sure this would actually be needed? I'll leave it for clarity
toContractA: ToContractStorage;
// The storage addresses actually used by the storage below will be different (handled silently), though it's the same 'variable name'.
// For backwards compatibility, it might be worth to be able to specify which is what I do in the parentheses.
#[component(address: b_address)]
toContractB: ToContractStorage;
#[component]
erc20: Erc20Storage<u64>;
}
impl ToA = LinkToContract<MyStorage, MyStorage::ToContractA>;
impl ToB = LinkToContract<MyStorage, MyStorage::ToContractB>;
// Declare our interfaces alongside their implementation. One outputs felts, one outputs u64, one outputs u256. They all use the same _generic_ implementation and the same storage.
impl FeltBasedInterface = ERC20<MyStorage, felt252, u64, MyStorage::erc20>; // Imaginary syntax to specofiy the 'derivation path' at the end.
impl u256BasedInterface = ERC20<MyStorage, u256, u64, MyStorage::erc20>;
impl u64BasedInterface = ERC20<MyStorage, u64, u64, MyStorage::erc20>;
}
// Can now call the above kinda like that:
MyContract { address, FeltBasedInterface }.balanceOf(owner: ContractAddress) -> felt262
MyContract { address, u256BasedInterface }.balanceOf(owner: ContractAddress) -> u256
MyContract { address, u64BasedInterface }.balanceOf(owner: ContractAddress) -> 64 |
Beta Was this translation helpful? Give feedback.
-
It seems confusing that the same syntax is used to make function calls and to emit events. The only differentiation is the capitalization, but that is by convention rather than enforced by the compiler. Here is an example using ERC-20 to illustrate my point, but I think it would be even harder to differentiate if both the function and event have the same input arguments.
It would be nice if function calls and emitting of events can be better differentiated instantly without double checking their definitions. For example, the Cairo 0.x syntax of |
Beta Was this translation helpful? Give feedback.
-
Suggestion of contract and ABI syntax based on traits.
ABI
Define the ABI separately as a trait. This is good for a few reasons:
Events
User code:
Under the hood, the derive will auto implement this trait:
The
from_keys_and_values
is more for outside tooling and inspection, as it doesn't really have a usein a contract.
Contract storage
Rust is data oriented, as opposed to object oriented. Basically, this means we strive to separate
data and functionality.
For example, structs and impls are separated.
Inspired by this, it makes sense in Cairo1 to separate contract storage from functionality (impls).
Benefits:
Storage variables
Constructor
Starknet core will have the trait:
User code:
Implementations
Multiple ABI implementations can be added ontop of a given storage.
Component
A component is a storage struct along with a generic impl that every contract can use. This is one kind on extensibility (the other being composition).
Then, a contract wishing to "extend" this component can just declare its impl using
Thi will impl the component abi for this contract.
Dispatcher
The
#[starknet::abi]
plugin would auto-generate a Dispatcher impl of the trait, that will look something like this:Note: The impl_selector is designed to avoid selector collisions between multiple impls for the same contract.
For example, a contract might want to implement ERC20 and ERC721, and they have the same function name.
A concrete suggestion is to use the name of the impl as the impl_selector.
Emitting events
The emitted events are a part of the interface of the contract - a part of the ABI.
They are indeed specified on the
#[abi(event=EventType)]
annotation.Since a contract might have multiple impls of abis, to separate different sets of events, the first
event key will be the
impl_selector
of the impl.Ideally, self.emit(event) will force the type of the event to be the type specified on the ABI.
This is not trivial, and left as an open problem for now.
Contract composition
Perhaps some plugin support for auto-generating a forwarding impl is required.
Contract class as an ensemble
A contract class is composed of a storage struct, and a set of ABI impls.
An ergonomic way to specify this set is by searching for all of the struct ABI impls in the current module.
An explicit way would be either externally (in the starknet cli, or some starknet config), managed by an outside tool.
It is also possible to specify the impls in some kind of annotation - not sure if it's desired.
Upgrade
In the latest starknet version, a new transaction was introduced to change the contract_class of a contract.
This is visioned to be the preferred way to upgrade a contract.
How to safely upgrade a contract, in a backwards compatible way?
There are two factors at play: ABI and state.
ABI
Since there might be other existing contracts calling our contract, we want to still support the exact same ABI as before.
A good way to do this is to keep the old
impl
s of our contract, with the same impl name (as it will server as theimpl_selector). Their functionality might be changed. We can also introduce new impls for the same or different ABIs,
to add new functionalities.
State
TODO:
Deployment and tooling.
A starknet deploy tool will need to:
Beta Was this translation helpful? Give feedback.
All reactions