-
Notifications
You must be signed in to change notification settings - Fork 71
Add documentation for implementing EnumerableSet #786
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
99ac5c3
80719cf
9fdc13f
2e7a3a4
b360335
7fe7c36
0d14b15
581d9ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,14 +1,56 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
//! Smart contract for managing sets. | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! Sets have the following properties: | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! * Elements are added, removed, and checked for existence in constant time | ||||||||||||||||||||||||||||||||||||||||||||||||
//! (O(1)). | ||||||||||||||||||||||||||||||||||||||||||||||||
//! * Elements are enumerated in O(n). No guarantees are made on the ordering. | ||||||||||||||||||||||||||||||||||||||||||||||||
//! * Set can be cleared (all elements removed) in O(n). | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! ## Usage | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! `EnumerableSet` works with many primitive types out of the box: | ||||||||||||||||||||||||||||||||||||||||||||||||
Peponks9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! ``` | ||||||||||||||||||||||||||||||||||||||||||||||||
//! extern crate alloc; | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! use alloy_primitives::{Address, U256}; | ||||||||||||||||||||||||||||||||||||||||||||||||
//! use stylus_sdk::prelude::*; | ||||||||||||||||||||||||||||||||||||||||||||||||
//! use openzeppelin_stylus::utils::structs::enumerable_set::EnumerableSet; | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! #[storage] | ||||||||||||||||||||||||||||||||||||||||||||||||
//! struct MyContract { | ||||||||||||||||||||||||||||||||||||||||||||||||
//! whitelist: EnumerableSet<Address>, | ||||||||||||||||||||||||||||||||||||||||||||||||
//! } | ||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unresolving, still missing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unresolving, still missing @Peponks9 I need to remind you to please not resolve change requests which have not been addressed. This is very important, as it makes for better clarity during code reviews. Only resolve change requests once:
|
||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! #[public] | ||||||||||||||||||||||||||||||||||||||||||||||||
//! impl MyContract { | ||||||||||||||||||||||||||||||||||||||||||||||||
//! fn add_to_whitelist(&mut self, address: Address) -> bool { | ||||||||||||||||||||||||||||||||||||||||||||||||
//! self.whitelist.add(address) | ||||||||||||||||||||||||||||||||||||||||||||||||
//! } | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! fn remove_from_whitelist(&mut self, address: Address) -> bool { | ||||||||||||||||||||||||||||||||||||||||||||||||
//! self.whitelist.remove(address) | ||||||||||||||||||||||||||||||||||||||||||||||||
//! } | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! fn is_whitelisted(&self, address: Address) -> bool { | ||||||||||||||||||||||||||||||||||||||||||||||||
//! self.whitelist.contains(address) | ||||||||||||||||||||||||||||||||||||||||||||||||
//! } | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! fn get_whitelist_size(&self) -> U256 { | ||||||||||||||||||||||||||||||||||||||||||||||||
//! self.whitelist.length() | ||||||||||||||||||||||||||||||||||||||||||||||||
//! } | ||||||||||||||||||||||||||||||||||||||||||||||||
//! } | ||||||||||||||||||||||||||||||||||||||||||||||||
//! ``` | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! ## Custom Storage Types | ||||||||||||||||||||||||||||||||||||||||||||||||
//! | ||||||||||||||||||||||||||||||||||||||||||||||||
//! You can implement `EnumerableSet` for your own storage types by implementing | ||||||||||||||||||||||||||||||||||||||||||||||||
//! the `Element` and `Accessor` traits. See `element.rs` for trait definitions | ||||||||||||||||||||||||||||||||||||||||||||||||
//! and the documentation for comprehensive examples. | ||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enclose in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unresolving, as types for not converted into links. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Roger that! Change done There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Change doesn't seem to be implemented, maybe you didn't push your branch There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leave a note below this stating that EnumerableSet can't currently be implemented for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unresolving, the change wasn't made. Please refrain from resolving comments that were not fully addressed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still not addressed |
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Change the mod-level doc to: //! Storage type for managing [sets] of primitive types.
//!
//! [sets]: https://en.wikipedia.org/wiki/Set_(abstract_data_type)
|
||||||||||||||||||||||||||||||||||||||||||||||||
pub mod element; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
/// Sets have the following properties: | ||||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||||
/// * Elements are added, removed, and checked for existence in constant | ||||||||||||||||||||||||||||||||||||||||||||||||
/// time (O(1)). | ||||||||||||||||||||||||||||||||||||||||||||||||
/// * Elements are enumerated in O(n). No guarantees are made on the | ||||||||||||||||||||||||||||||||||||||||||||||||
/// ordering. | ||||||||||||||||||||||||||||||||||||||||||||||||
/// * Set can be cleared (all elements removed) in O(n). | ||||||||||||||||||||||||||||||||||||||||||||||||
use alloc::{vec, vec::Vec}; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
use alloy_primitives::{uint, U256}; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -188,6 +230,8 @@ mod tests { | |||||||||||||||||||||||||||||||||||||||||||||||
use stylus_sdk::prelude::TopLevelStorage; | ||||||||||||||||||||||||||||||||||||||||||||||||
use alloy_primitives::private::proptest::{prop_assert, prop_assert_eq, proptest}; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
extern crate alloc; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
Peponks9 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't seem to find this one, on lines provided it is not after There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe you didn't push local changes? |
||||||||||||||||||||||||||||||||||||||||||||||||
unsafe impl TopLevelStorage for $set_type {} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
#[public] | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
= EnumerableSet | ||
Peponks9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
The `EnumerableSet` utility in OpenZeppelin Stylus Contracts provides an efficient way to manage sets of values in smart contracts. While it comes with built-in support for many primitive types like `Address`, `U256`, and `B256`, you can also implement it for your own custom storage types by implementing the required traits. | ||
|
||
[[overview]] | ||
== Overview | ||
|
||
`EnumerableSet<T>` is a generic data structure that provides O(1) time complexity for adding, removing, and checking element existence, while allowing enumeration of all elements in O(n) time. The generic type `T` must implement the `Element` trait, which associates the element type with its corresponding storage type. | ||
|
||
[[built-in-types]] | ||
== Built-in Supported Types | ||
|
||
The following types are already supported out of the box: | ||
|
||
- `Address` → `StorageAddress` | ||
- `B256` → `StorageB256` | ||
- `U8` → `StorageU8` | ||
- `U16` → `StorageU16` | ||
- `U32` → `StorageU32` | ||
- `U64` → `StorageU64` | ||
- `U128` → `StorageU128` | ||
- `U256` → `StorageU256` | ||
Peponks9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
[[custom-implementation]] | ||
== Implementing for Custom Storage Types | ||
|
||
To use `EnumerableSet` with your own storage types, you need to implement two traits: | ||
|
||
1. **`Element`** - Associates your element type with its storage type | ||
2. **`Accessor`** - Provides getter and setter methods for the storage type | ||
|
||
[[element-trait]] | ||
=== The Element Trait | ||
Peponks9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
The `Element` trait requires your type to implement `StorageKey + Copy` and defines an associated type `StorageElement`: | ||
|
||
[source,rust] | ||
---- | ||
pub trait Element: StorageKey + Copy { | ||
/// Set element type in storage. | ||
type StorageElement: for<'a> StorageType<Wraps<'a> = Self> | ||
+ Accessor<Wraps = Self> | ||
+ for<'a> SimpleStorageType<'a> | ||
+ Erase; | ||
} | ||
---- | ||
|
||
[[accessor-trait]] | ||
=== The Accessor Trait | ||
|
||
The `Accessor` trait provides the interface for reading and writing values to storage: | ||
|
||
[source,rust] | ||
---- | ||
pub trait Accessor { | ||
/// Type of the number associated with the storage type. | ||
type Wraps; | ||
|
||
/// Gets underlying element from persistent storage. | ||
fn get(&self) -> Self::Wraps; | ||
|
||
/// Sets underlying element in persistent storage. | ||
fn set(&mut self, value: Self::Wraps); | ||
} | ||
---- | ||
|
||
[[implementation-example]] | ||
== Complete Implementation Example | ||
|
||
Here's a complete example showing how to implement `EnumerableSet` for a custom `User` struct: | ||
|
||
[source,rust] | ||
Peponks9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
---- | ||
use openzeppelin_stylus::{ | ||
utils::structs::enumerable_set::{EnumerableSet, Element, Accessor}, | ||
prelude::*, | ||
}; | ||
use stylus_sdk::storage::{StorageMap, StorageU256, StorageVec}; | ||
|
||
// Define your custom struct | ||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
struct User { | ||
id: U256, | ||
name: String, | ||
} | ||
|
||
// Define the storage type for User | ||
#[storage] | ||
struct StorageUser { | ||
id: StorageU256, | ||
name: StorageString, | ||
} | ||
|
||
// Implement Element trait for User | ||
impl Element for User { | ||
type StorageElement = StorageUser; | ||
} | ||
|
||
// Implement Accessor trait for StorageUser | ||
impl Accessor for StorageUser { | ||
type Wraps = User; | ||
|
||
fn get(&self) -> Self::Wraps { | ||
User { | ||
id: self.id.get(), | ||
name: self.name.get(), | ||
} | ||
} | ||
|
||
fn set(&mut self, value: Self::Wraps) { | ||
self.id.set(value.id); | ||
self.name.set(value.name); | ||
} | ||
} | ||
|
||
// Now you can use EnumerableSet<User> in your contract | ||
#[storage] | ||
struct MyContract { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing entrypoint attr |
||
users: EnumerableSet<User>, | ||
user_count: StorageU256, | ||
} | ||
|
||
#[public] | ||
impl MyContract { | ||
fn add_user(&mut self, user: User) -> bool { | ||
let added = self.users.add(user); | ||
if added { | ||
self.user_count.set(self.user_count.get() + U256::from(1)); | ||
} | ||
added | ||
} | ||
|
||
fn remove_user(&mut self, user: User) -> bool { | ||
let removed = self.users.remove(user); | ||
if removed { | ||
self.user_count.set(self.user_count.get() - U256::from(1)); | ||
} | ||
removed | ||
} | ||
|
||
fn get_user_at(&self, index: U256) -> Option<User> { | ||
self.users.at(index) | ||
} | ||
|
||
fn get_all_users(&self) -> Vec<User> { | ||
self.users.values() | ||
} | ||
|
||
fn user_count(&self) -> U256 { | ||
self.user_count.get() | ||
} | ||
} | ||
---- | ||
|
||
[[limitations]] | ||
== Current Limitations | ||
|
||
**Note:** `StorageBytes` and `StorageString` cannot currently be implemented for `EnumerableSet` due to limitations in the Stylus SDK. This limitations may change in future versions of the Stylus SDK. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the sentence to state that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made the changes but didn't find a link for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both links added There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The changes haven't been made or haven't been pushed, you can resolve this conversation together with https://github.com/OpenZeppelin/rust-contracts-stylus/pull/786/files#r2281442336 |
||
|
||
|
||
[[best-practices]] | ||
== Best Practices | ||
|
||
1. **Keep element types small**: Since `EnumerableSet` stores all elements in storage, large element types will increase gas costs significantly. | ||
|
||
2. **Use appropriate storage types**: Choose storage types that efficiently represent your data. For example, use `StorageU64` instead of `StorageU256` if your values fit in 64 bits. | ||
|
||
3. **Consider gas costs**: Each operation (add, remove, contains) has a gas cost. For frequently accessed sets, consider caching frequently used values in memory. | ||
|
||
4. **Test thoroughly**: Use property-based testing to ensure your custom implementation maintains the mathematical properties of sets (idempotency, commutativity, associativity, etc.). | ||
|
||
[[testing]] | ||
== Testing Your Implementation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove this section, devs can't run OZ tests from within their projects There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unresolving, the cargo test script is still present There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Peponks9 another reminder not to resolve conversations that haven't been fully addressed and the changes pushed. |
||
|
||
The built-in tests for `EnumerableSet` use property-based testing to verify set properties. You can run these tests to ensure your custom implementation works correctly: | ||
|
||
[source,bash] | ||
---- | ||
cargo test --package openzeppelin-stylus-contracts --test enumerable_set | ||
---- | ||
|
||
[[advanced-usage]] | ||
== Advanced Usage Patterns | ||
|
||
=== Role-based Access Control | ||
|
||
`EnumerableSet` is commonly used in access control systems to manage role members: | ||
|
||
[source,rust] | ||
---- | ||
#[storage] | ||
struct AccessControl { | ||
role_members: StorageMap<B256, EnumerableSet<Address>>, | ||
} | ||
|
||
impl AccessControl { | ||
fn grant_role(&mut self, role: B256, account: Address) { | ||
self.role_members.get(role).add(account); | ||
} | ||
|
||
fn revoke_role(&mut self, role: B256, account: Address) { | ||
self.role_members.get(role).remove(account); | ||
} | ||
|
||
fn get_role_members(&self, role: B256) -> Vec<Address> { | ||
self.role_members.get(role).values() | ||
} | ||
} | ||
Comment on lines
+268
to
+285
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Provide a full working contract example, including imports. Devs should be able to copy/paste this example and have it deployable out of the box. |
||
---- | ||
|
||
=== Whitelist Management | ||
|
||
Manage whitelisted addresses efficiently: | ||
|
||
[source,rust] | ||
---- | ||
#[storage] | ||
struct Whitelist { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same for this, make it fully working example |
||
allowed_addresses: EnumerableSet<Address>, | ||
max_whitelist_size: StorageU256, | ||
} | ||
|
||
impl Whitelist { | ||
fn add_to_whitelist(&mut self, address: Address) -> Result<(), String> { | ||
if self.allowed_addresses.length() >= self.max_whitelist_size.get() { | ||
return Err("Whitelist is full".to_string()); | ||
} | ||
|
||
if self.allowed_addresses.add(address) { | ||
Ok(()) | ||
} else { | ||
Err("Address already in whitelist".to_string()) | ||
} | ||
} | ||
} | ||
---- | ||
|
||
[[conclusion]] | ||
== Conclusion | ||
Peponks9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
By implementing the `Element` and `Accessor` traits, you can extend `EnumerableSet` to work with any custom storage type that meets the requirements. This provides a powerful and flexible way to manage sets of complex data structures in your Stylus smart contracts while maintaining the performance characteristics of the underlying implementation. | ||
|
||
Remember to consider gas costs and storage efficiency when designing your custom types, and always test thoroughly to ensure correctness. |
Uh oh!
There was an error while loading. Please reload this page.