Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 51 additions & 7 deletions contracts/src/utils/structs/enumerable_set/mod.rs
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:
//!
//! ```
//! 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>,
//! }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
//! ```
//! 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>,
//! }
//! ```rust
//! extern crate alloc;
//!
//! use alloy_primitives::{Address, U256};
//! use stylus_sdk::prelude::*;
//! use openzeppelin_stylus::utils::structs::enumerable_set::EnumerableSet;
//!
//! #[storage]
//! #[entrypoint]
//! struct MyContract {
//! whitelist: EnumerableSet<Address>,
//! }

Copy link
Collaborator

Choose a reason for hiding this comment

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

Unresolving, still missing #[entrypoint].

Copy link
Collaborator

@0xNeshi 0xNeshi Aug 19, 2025

Choose a reason for hiding this comment

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

Unresolving, still missing #[entrypoint].

@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:

  1. The change request is addressed (implemented).
  2. Your local changes are pushed to remote and PR is updated.

//!
//! #[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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Enclose in [..] to create links to these types/files

Copy link
Collaborator

Choose a reason for hiding this comment

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

Unresolving, as types for not converted into links.

Copy link
Author

Choose a reason for hiding this comment

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

Roger that! Change done

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 Bytes and String due to current SDK limitations

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Still not addressed


Copy link
Collaborator

Choose a reason for hiding this comment

The 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};
Expand Down Expand Up @@ -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;

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change

Copy link
Author

Choose a reason for hiding this comment

The 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 use alloy_primitives::private::proptest::{prop_assert, prop_assert_eq, proptest}; anymore.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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]
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
** xref:uups-proxy.adoc[UUPS Proxy]

* xref:utilities.adoc[Utilities]
** xref:enumerable-set-custom.adoc[EnumerableSet]
243 changes: 243 additions & 0 deletions docs/modules/ROOT/pages/enumerable-set-custom.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
= EnumerableSet

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`

[[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

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]
----
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Update the sentence to state that Bytes and String cannot currently be implemented due to SDK limitation. Link to the actual types.

Copy link
Author

Choose a reason for hiding this comment

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

Made the changes but didn't find a link for String, just Bytes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

Both links added

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Unresolving, the cargo test script is still present

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

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.
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/utilities.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ Contracts for Stylus provides these libraries for enhanced data structure manage

- https://docs.rs/openzeppelin-stylus/0.3.0-rc.1/openzeppelin_stylus/utils/structs/bitmap/index.html[`BitMaps`]: Store packed booleans in storage.
- https://docs.rs/openzeppelin-stylus/0.3.0-rc.1/openzeppelin_stylus/utils/structs/checkpoints/index.html[`Checkpoints`]: Checkpoint values with built-in lookups.
- https://docs.rs/openzeppelin-stylus/0.3.0-rc.1/openzeppelin_stylus/utils/structs/enumerable_set/index.html[`EnumerableSets`]: Contract for managing sets of many primitive types.
- https://docs.rs/openzeppelin-stylus/0.3.0-rc.1/openzeppelin_stylus/utils/structs/enumerable_set/index.html[`EnumerableSets`]: Contract for managing sets of many primitive types. For information on implementing with custom storage types, see xref:enumerable-set-custom.adoc[EnumerableSet Custom Types].