Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: storage layout + read/write slots + mut keyword #30

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

0xrusowsky
Copy link
Contributor

  • enforces mut keyword for write operations, so that the compiler complains when a contract fn tries to update storage without &mut self
  • implements wrapper type Slot<V> for primitives that fit a single storage slot (equivalent to the old Word<V> >>> as discussed, we don't like this wrapper type, so i think that down the line we can enhance the contract macro so that the devex is like working with primitives.
  • implements a new proc macro storage that initializes the storage layout >>> it requires all the attributes of the struct to implement the StorageLayout trait (some notes regarding this point):
    • maybe the StorageLayout trait is unnecessary and it could be merged with the existing StorageStorable.
    • i still haven't thought how we will deal with constants (since they should be retrieved from the bytecode), but probably we can change the slot id from u64 to Option<u64> or something like this.
    • we could probably enhance this macro to autogenerate getter fns for all of its attributes
    • i will add tests to validate that the slots are properly initialized and that there is no storage collisions once the design is validated

PS: i know i'm not the best at coming up with good names, so feel free to propose new ones

name: String,
symbol: String,
decimals: u8,
total_supply: Slot<U256>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We chatted about some of this offline, I think it's a bit odd to have Slot here but not in Mapping, but good for a first version and can be improved in the future.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I understand correctly, every type used in a struct that derives storage has to implement StorageLayout, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We chatted about some of this offline, I think it's a bit odd to have Slot here but not in Mapping, but good for a first version and can be improved in the future.

yes, my bad. I totally forgot... I'll amend the PR to incorporate that design pattern.

So if I understand correctly, every type used in a struct that derives storage has to implement StorageLayout, right?

yes, that's correct they need to impl StorageLayout so that we can ensure that we can allocate a slot in the layout for them.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine as is and we can iterate later on

pub fn total_supply(&self) -> U256 {
self.total_supply
self.total_supply.read()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you wrote this somewhere, but pub fields could also get automatic getters, can be done in the future. (let's open an issue)

fn call(&self);
fn call_with_data(&self, calldata: &[u8]);
fn call(&mut self);
fn call_with_data(&mut self, calldata: &[u8]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could also make sense later on to have immutable versions of these, not sure how that would work but it'd be cool.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed.
do you foresee any scenario other than static calls where we would use the not mutable call?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not right now

padded[..bytes.len()].copy_from_slice(&bytes);
sstore(key, U256::from_be_bytes(padded));
impl<K, V> StorageLayout for Mapping<K, V> {
fn allocate(slot: u64) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

/// Wrapper around `alloy::primitives` that can be written in a single slot (single EVM word).
#[derive(Default)]
pub struct Slot<V> {
id: u64,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, shouldn't the id be u256? I agree it should be fine as u64 if we incrementally allocate slots, but to be fully compatible/flexible and allow custom storage layouts from the user I think this would have to be u256?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it should be u256, i just kept the u64 as that's what was being used in Mapping. I'll do a follow-up commit to make everything compliant.

});

// Generate initialization code for each field
let init_fields = fields.iter().enumerate().map(|(slot, f)| {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine for now, but we should spend some time crafting how storage layout will work. For example, this won't allow for storing a (u256, u256) sequentially, we'd need to implement StorageLayout for it with a phantom slot, and then in the actual read/write use some encoded key (similar to Mapping). Ideally we'd know the storage size of the type at compile time (if value type) and use that size for allocation instead of enumerate. Reference types (Mapping, arrays, etc) could work like Mapping. This is what Solidity does also.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really good point and i totally missed that. Do you want me to try to address it in this PR? or would u open an issue and do it in the following one?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think later is fine

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants