Skip to content
65 changes: 57 additions & 8 deletions contracts/src/utils/structs/enumerable_set/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@

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::U256;
Expand All @@ -18,7 +11,63 @@ use stylus_sdk::{
storage::{StorageMap, StorageType, StorageU256, StorageVec},
};

/// State of an [`EnumerableSet`] contract.
/// 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
Comment on lines +14 to +21
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
/// 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
/// Storage type for managing [sets] of primitive types.
///
/// 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).
///
/// [sets]: https://en.wikipedia.org/wiki/Set_(abstract_data_type)
///
/// ## Usage

///
/// `EnumerableSet` works with the following primitive types out of the box:
///
/// * [`alloy_primitives::Address`] - Ethereum addresses
/// * [`alloy_primitives::B256`] - 256-bit byte arrays
/// * [`alloy_primitives::U8`] - 8-bit unsigned integers
/// * [`alloy_primitives::U16`] - 16-bit unsigned integers
/// * [`alloy_primitives::U32`] - 32-bit unsigned integers
/// * [`alloy_primitives::U64`] - 64-bit unsigned integers
/// * [`alloy_primitives::U128`] - 128-bit unsigned integers
/// * [`alloy_primitives::U256`] - 256-bit unsigned integers
///
/// ```rust
/// 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>,
/// }
///
/// #[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.
#[storage]
pub struct EnumerableSet<T: Element> {
/// Values in the set.
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 Implementation for Custom Storage Types]
313 changes: 313 additions & 0 deletions docs/modules/ROOT/pages/enumerable-set-custom.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
= EnumerableSet Implementation for Custom Storage Types

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:

[[built-in-types]]
== Built-in Supported Types

The following types are already supported out of the box:
Comment on lines +10 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove duplicate section above


- https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Address.html[`Address`] → https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/struct.StorageAddress.html[`StorageAddress`]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Update all links to use the actually used versions instead of latest, to avoid confusing devs when the currently used version behavior differs from the latest version's.
E.g. this line becomes:

- https://docs.rs/alloy-primitives/0.8.20/alloy_primitives/struct.Address.html[`Address`] → https://docs.rs/stylus-sdk/0.9.0/stylus_sdk/storage/struct.StorageAddress.html[`StorageAddress`]

- https://docs.rs/alloy-primitives/latest/alloy_primitives/aliases/type.B256.html[`B256`] → https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/type.StorageB256.html[`StorageB256`]
- https://docs.rs/alloy-primitives/latest/alloy_primitives/aliases/type.U8.html[`U8`] → https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/type.StorageU8.html[`StorageU8`]
- https://docs.rs/alloy-primitives/latest/alloy_primitives/aliases/type.U16.html[`U16`] → https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/type.StorageU16.html[`StorageU16`]
- https://docs.rs/alloy-primitives/latest/alloy_primitives/aliases/type.U32.html[`U32`] → https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/type.StorageU32.html[`StorageU32`]
- https://docs.rs/alloy-primitives/latest/alloy_primitives/aliases/type.U64.html[`U64`] → https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/type.StorageU64.html[`StorageU64`]
- https://docs.rs/alloy-primitives/latest/alloy_primitives/aliases/type.U128.html[`U128`] → https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/type.StorageU128.html[`StorageU128`]
- hhttps://docs.rs/alloy-primitives/latest/alloy_primitives/aliases/type.U256.html[`U256`] → https://docs.rs/stylus-sdk/latest/stylus_sdk/storage/type.StorageU256.html[`StorageU256`]

[[custom-implementation]]
== Implementing for Custom Storage Types

To use `EnumerableSet` with your own storage types, you need to implement two traits:

1. https://docs.rs/openzeppelin-stylus/0.3.0-rc.1/openzeppelin_stylus/utils/structs/enumerable_set/element/trait.Element.html[`Element`] - Associates your element type with its storage type
2. https://docs.rs/openzeppelin-stylus/0.3.0-rc.1/openzeppelin_stylus/utils/structs/enumerable_set/element/trait.Accessor.html[`Accessor`] - Provides getter and setter methods for the storage type

[[implementation-example]]
== Step-by-Step Implementation

Let's implement `EnumerableSet` for a custom `User` struct by breaking it down into clear steps:

=== Step 1: Define Your Custom Type

First, define your custom struct that will be stored in the set. It must implement the required traits for hashing and comparison:

[source,rust]
----
use alloy_primitives::U256;
use stylus_sdk::prelude::*;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Move this import to Step 2

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct User {
id: U256,
role: u8,
}
----

=== Step 2: Create a Storage Wrapper

Create a storage struct that mirrors your custom type using Stylus storage types:

[source,rust]
----
use stylus_sdk::storage::{StorageU256, StorageU8};
#[storage]
struct StorageUser {
id: StorageU256,
role: StorageU8,
}
----

=== Step 3: Implement the Required Traits

Implement both the `Element` and `Accessor` traits to connect your type with its storage representation:

[source,rust]
----
use openzeppelin_stylus::utils::structs::enumerable_set::{Element, Accessor};
// Connect User with its storage type
impl Element for User {
type StorageElement = StorageUser;
}
// Provide get/set methods for the storage type
impl Accessor for StorageUser {
type Wraps = User;
fn get(&self) -> Self::Wraps {
User {
id: self.id.get(),
role: self.role.get(),
}
}
fn set(&mut self, value: Self::Wraps) {
self.id.set(value.id);
self.role.set(value.role);
}
}
----

=== Step 4: Use Your Custom EnumerableSet

Now you can use `EnumerableSet<User>` in your smart contract:

[source,rust]
----
use openzeppelin_stylus::utils::structs::enumerable_set::EnumerableSet;
#[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,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Redundant field, as user count is stored in the EnumerableSet itself (see: EnumerableSet::length).

let user_count = self.users.length();

}
#[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()
}
}
----

=== Complete Implementation Example
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does the full example compile?


Here's the complete code putting all the steps together:

[source,rust]
----
use openzeppelin_stylus::{
utils::structs::enumerable_set::{EnumerableSet, Element, Accessor},
prelude::*,
};
use stylus_sdk::storage::{StorageU256, StorageU8};
use alloy_primitives::U256;
// Step 1: Define your custom struct
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remove step comments from the full impl example

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct User {
id: U256,
role: u8,
}
// Step 2: Define the storage type for User
#[storage]
struct StorageUser {
id: StorageU256,
role: StorageU8,
}
// Step 3: Implement Element trait for User
impl Element for User {
type StorageElement = StorageUser;
}
// Step 3: Implement Accessor trait for StorageUser
impl Accessor for StorageUser {
type Wraps = User;
fn get(&self) -> Self::Wraps {
User {
id: self.id.get(),
role: self.role.get(),
}
}
fn set(&mut self, value: Self::Wraps) {
self.id.set(value.id);
self.role.set(value.role);
}
}
// Step 4: 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:** https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Bytes.html[`Bytes`] and `String` cannot currently be implemented for `EnumerableSet` due to limitations in the Stylus SDK. These 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.

Suggested change
**Note:** https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Bytes.html[`Bytes`] and `String` cannot currently be implemented for `EnumerableSet` due to limitations in the Stylus SDK. These limitations may change in future versions of the Stylus SDK.
https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Bytes.html[`Bytes`] and `String` cannot currently be implemented for `EnumerableSet` due to limitations in the Stylus SDK. These limitations may change in future versions of the Stylus SDK.

nit: redundant, already in a separate section.

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
**Note:** https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Bytes.html[`Bytes`] and `String` cannot currently be implemented for `EnumerableSet` due to limitations in the Stylus SDK. These limitations may change in future versions of the Stylus SDK.
**Note:** https://docs.rs/alloy-primitives/latest/alloy_primitives/struct.Bytes.html[`Bytes`] and https://doc.rust-lang.org/stable/alloc/string/struct.String.html[`String`] cannot currently be implemented for `EnumerableSet` due to limitations in the Stylus SDK. These limitations may change in future versions of the Stylus SDK.


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

[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())
}
}
}
----
Loading
Loading