Repository: OpenZeppelin/rust-contracts-stylus (rust)

Issue:
Rename `e2e::User` to `e2e::Account`
Rationale: it's slightly more accurate and consistent with the rest of the ecosystem (`EOA`).

Pull request:
refactor: rename `e2e::User` to `e2e::Account`
<!--
Thank you for your interest in contributing to OpenZeppelin!

Consider opening an issue for discussion prior to submitting a PR. New features will be merged faster if they were first discussed and designed with the team.

Describe the changes introduced in this pull request. Include any context necessary for understanding the PR's purpose.
-->

Renamed the `e2e::User` module to `e2e::Account` maintaining consistency with the rest of the ecosystem (EOA). 


<!-- Fill in with issue number -->
Resolves #138 

#### PR Checklist

<!--
Before merging the pull request all of the following must be completed.
Feel free to submit a PR or Draft PR even if some items are pending.
Some of the items may not apply.
-->

- [x] Tests
- [x] Documentation


Patch:
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 84e1ee6..1164b11 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -106,4 +106,4 @@ jobs:
       - name: Checkout Actions Repository
         uses: actions/checkout@v4
       - name: Check spelling of files in the workspace
-        uses: crate-ci/typos@v1.22.7
+        uses: crate-ci/typos@v1.22.9
diff --git a/contracts/src/arithmetic.rs b/contracts/src/arithmetic.rs
deleted file mode 100644
index e4cd10d..0000000
--- a/contracts/src/arithmetic.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-use alloy_primitives::U256;
-use stylus_sdk::storage::{StorageGuardMut, StorageUint};
-
-pub(crate) trait AddAssignUnchecked<T> {
-    fn add_assign_unchecked(&mut self, rhs: T);
-}
-
-impl<'a> AddAssignUnchecked<U256> for StorageGuardMut<'a, StorageUint<256, 4>> {
-    fn add_assign_unchecked(&mut self, rhs: U256) {
-        let new_balance = self.get() + rhs;
-        self.set(new_balance);
-    }
-}
-
-pub(crate) trait SubAssignUnchecked<T> {
-    fn sub_assign_unchecked(&mut self, rhs: T);
-}
-
-impl<'a> SubAssignUnchecked<U256> for StorageGuardMut<'a, StorageUint<256, 4>> {
-    fn sub_assign_unchecked(&mut self, rhs: U256) {
-        let new_balance = self.get() - rhs;
-        self.set(new_balance);
-    }
-}
diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs
index c67fba2..432d454 100644
--- a/contracts/src/lib.rs
+++ b/contracts/src/lib.rs
@@ -8,7 +8,6 @@ static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
 
 #[cfg(any(feature = "std", feature = "access"))]
 pub mod access;
-mod arithmetic;
 
 pub mod token;
 
diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs
index f251185..e7ff3b9 100644
--- a/contracts/src/token/erc721/mod.rs
+++ b/contracts/src/token/erc721/mod.rs
@@ -6,7 +6,7 @@ use stylus_sdk::{
     abi::Bytes, alloy_sol_types::sol, call::Call, evm, msg, prelude::*,
 };
 
-use crate::arithmetic::{AddAssignUnchecked, SubAssignUnchecked};
+use crate::utils::math::storage::{AddAssignUnchecked, SubAssignUnchecked};
 
 pub mod extensions;
 
diff --git a/contracts/src/utils/math/alloy.rs b/contracts/src/utils/math/alloy.rs
new file mode 100644
index 0000000..c3f3855
--- /dev/null
+++ b/contracts/src/utils/math/alloy.rs
@@ -0,0 +1,172 @@
+//! Standard math utilities missing in `alloy_primitives`.
+use alloy_primitives::{uint, U256};
+
+/// Trait for standard math utilities missing in `alloy_primitives`.
+pub trait Math {
+    /// Returns the square root of a number. If the number is not a perfect
+    /// square, the value is rounded towards zero.
+    /// This method is based on Newton's method for computing square roots; the
+    /// algorithm is restricted to only using integer operations.
+    ///
+    /// # Arguments
+    ///
+    /// * `self` - value to perform square root operation onto.
+    #[must_use]
+    fn sqrt(self) -> Self;
+
+    /// Returns the average of two numbers. The result is rounded towards zero.
+    ///
+    /// # Arguments
+    ///
+    /// * `self` - first value to compute average.
+    /// * `rhs` - second value to compute average.
+    #[must_use]
+    fn average(self, rhs: Self) -> Self;
+}
+
+impl Math for U256 {
+    fn sqrt(self) -> Self {
+        let a = self;
+        let one = uint!(1_U256);
+        if a <= one {
+            return a;
+        }
+
+        // In this function, we use Newton's method to get a root of `f(x) := x²
+        // - a`. It involves building a sequence x_n that converges
+        // toward sqrt(a). For each iteration x_n, we also define the
+        // error between the current value as `ε_n = | x_n - sqrt(a) |`.
+        //
+        // For our first estimation, we consider `e` the smallest power of 2
+        // which is bigger than the square root of the target. (i.e.
+        // `2**(e-1) ≤ sqrt(a) < 2**e`). We know that `e ≤ 128` because
+        // `(2¹²⁸)² = 2²⁵⁶` is bigger than any uint256.
+        //
+        // By noticing that
+        // `2**(e-1) ≤ sqrt(a) < 2**e → (2**(e-1))² ≤ a < (2**e)² → 2**(2*e-2) ≤
+        // a < 2**(2*e)` we can deduce that `e - 1` is `log2(a) / 2`. We
+        // can thus compute `x_n = 2**(e-1)` using a method similar to
+        // the msb function.
+        let mut aa = a;
+        let mut xn = one;
+
+        if aa >= (one << 128) {
+            aa >>= 128;
+            xn <<= 64;
+        }
+        if aa >= (one << 64) {
+            aa >>= 64;
+            xn <<= 32;
+        }
+        if aa >= (one << 32) {
+            aa >>= 32;
+            xn <<= 16;
+        }
+        if aa >= (one << 16) {
+            aa >>= 16;
+            xn <<= 8;
+        }
+        if aa >= (one << 8) {
+            aa >>= 8;
+            xn <<= 4;
+        }
+        if aa >= (one << 4) {
+            aa >>= 4;
+            xn <<= 2;
+        }
+        if aa >= (one << 2) {
+            xn <<= 1;
+        }
+
+        // We now have x_n such that `x_n = 2**(e-1) ≤ sqrt(a) < 2**e = 2 *
+        // x_n`. This implies ε_n ≤ 2**(e-1).
+        //
+        // We can refine our estimation by noticing that the middle of that
+        // interval minimizes the error. If we move x_n to equal
+        // 2**(e-1) + 2**(e-2), then we reduce the error to ε_n ≤
+        // 2**(e-2). This is going to be our x_0 (and ε_0)
+        xn = (uint!(3_U256) * xn) >> 1; // ε_0 := | x_0 - sqrt(a) | ≤ 2**(e-2)
+
+        // From here, Newton's method give us:
+        // x_{n+1} = (x_n + a / x_n) / 2
+        //
+        // One should note that:
+        // x_{n+1}² - a = ((x_n + a / x_n) / 2)² - a
+        //              = ((x_n² + a) / (2 * x_n))² - a
+        //              = (x_n⁴ + 2 * a * x_n² + a²) / (4 * x_n²) - a
+        //              = (x_n⁴ + 2 * a * x_n² + a² - 4 * a * x_n²) / (4 * x_n²)
+        //              = (x_n⁴ - 2 * a * x_n² + a²) / (4 * x_n²)
+        //              = (x_n² - a)² / (2 * x_n)²
+        //              = ((x_n² - a) / (2 * x_n))²
+        //              ≥ 0
+        // Which proves that for all n ≥ 1, sqrt(a) ≤ x_n
+        //
+        // This gives us the proof of quadratic convergence of the sequence:
+        // ε_{n+1} = | x_{n+1} - sqrt(a) |
+        //         = | (x_n + a / x_n) / 2 - sqrt(a) |
+        //         = | (x_n² + a - 2*x_n*sqrt(a)) / (2 * x_n) |
+        //         = | (x_n - sqrt(a))² / (2 * x_n) |
+        //         = | ε_n² / (2 * x_n) |
+        //         = ε_n² / | (2 * x_n) |
+        //
+        // For the first iteration, we have a special case where x_0 is known:
+        // ε_1 = ε_0² / | (2 * x_0) |
+        //     ≤ (2**(e-2))² / (2 * (2**(e-1) + 2**(e-2)))
+        //     ≤ 2**(2*e-4) / (3 * 2**(e-1))
+        //     ≤ 2**(e-3) / 3
+        //     ≤ 2**(e-3-log2(3))
+        //     ≤ 2**(e-4.5)
+        //
+        // For the following iterations, we use the fact that, 2**(e-1) ≤
+        // sqrt(a) ≤ x_n: ε_{n+1} = ε_n² / | (2 * x_n) |
+        //         ≤ (2**(e-k))² / (2 * 2**(e-1))
+        //         ≤ 2**(2*e-2*k) / 2**e
+        //         ≤ 2**(e-2*k)
+        xn = (xn + a / xn) >> 1; // ε_1 := | x_1 - sqrt(a) | ≤ 2**(e-4.5)  -- special case, see above
+        xn = (xn + a / xn) >> 1; // ε_2 := | x_2 - sqrt(a) | ≤ 2**(e-9)    -- general case with k = 4.5
+        xn = (xn + a / xn) >> 1; // ε_3 := | x_3 - sqrt(a) | ≤ 2**(e-18)   -- general case with k = 9
+        xn = (xn + a / xn) >> 1; // ε_4 := | x_4 - sqrt(a) | ≤ 2**(e-36)   -- general case with k = 18
+        xn = (xn + a / xn) >> 1; // ε_5 := | x_5 - sqrt(a) | ≤ 2**(e-72)   -- general case with k = 36
+        xn = (xn + a / xn) >> 1; // ε_6 := | x_6 - sqrt(a) | ≤ 2**(e-144)  -- general case with k = 72
+
+        // Because e ≤ 128 (as discussed during the first estimation phase), we
+        // know have reached a precision ε_6 ≤ 2**(e-144) < 1. Given
+        // we're operating on integers, then we can ensure that xn is
+        // now either sqrt(a) or sqrt(a) + 1.
+        xn - U256::from(xn > a / xn)
+    }
+
+    fn average(self, rhs: Self) -> Self {
+        // `(a + b) / 2` can overflow, so instead we compute
+        // `(2 * (a & b) + (a ^ b)) / 2`.
+        //
+        // `a ^ b` computes the sum without carries while `2 * (a & b)` singles
+        // out the carries, so `2 * (a & b) + (a ^ b) == a + b`. Sum with no
+        // carries + carries.
+        (self & rhs) + ((self ^ rhs) >> 1)
+    }
+}
+
+#[cfg(all(test, feature = "std"))]
+mod tests {
+    use alloy_primitives::{private::proptest::proptest, uint, U256, U512};
+
+    use crate::utils::math::alloy::Math;
+
+    #[test]
+    fn check_sqrt() {
+        proptest!(|(value: U256)| {
+            // U256::root(..) method requires std. Can only be used in tests.
+            assert_eq!(value.sqrt(), value.root(2));
+        });
+    }
+
+    #[test]
+    fn check_average() {
+        proptest!(|(left: U256, right: U256)| {
+            // compute average in straight forward way with overflow and downcast.
+            let expected = (U512::from(left) + U512::from(right)) / uint!(2_U512);
+            assert_eq!(left.average(right), U256::from(expected));
+        });
+    }
+}
diff --git a/contracts/src/utils/math/mod.rs b/contracts/src/utils/math/mod.rs
new file mode 100644
index 0000000..db60b58
--- /dev/null
+++ b/contracts/src/utils/math/mod.rs
@@ -0,0 +1,3 @@
+//! Math helpers for `alloy` and Solidity storage types.
+pub mod alloy;
+pub mod storage;
diff --git a/contracts/src/utils/math/storage.rs b/contracts/src/utils/math/storage.rs
new file mode 100644
index 0000000..31193c4
--- /dev/null
+++ b/contracts/src/utils/math/storage.rs
@@ -0,0 +1,29 @@
+//! Simple math operations missing in `stylus_sdk::storage`.
+use alloy_primitives::Uint;
+use stylus_sdk::storage::{StorageGuardMut, StorageUint};
+
+pub(crate) trait AddAssignUnchecked<T> {
+    fn add_assign_unchecked(&mut self, rhs: T);
+}
+
+impl<'a, const B: usize, const L: usize> AddAssignUnchecked<Uint<B, L>>
+    for StorageGuardMut<'a, StorageUint<B, L>>
+{
+    fn add_assign_unchecked(&mut self, rhs: Uint<B, L>) {
+        let new_balance = self.get() + rhs;
+        self.set(new_balance);
+    }
+}
+
+pub(crate) trait SubAssignUnchecked<T> {
+    fn sub_assign_unchecked(&mut self, rhs: T);
+}
+
+impl<'a, const B: usize, const L: usize> SubAssignUnchecked<Uint<B, L>>
+    for StorageGuardMut<'a, StorageUint<B, L>>
+{
+    fn sub_assign_unchecked(&mut self, rhs: Uint<B, L>) {
+        let new_balance = self.get() - rhs;
+        self.set(new_balance);
+    }
+}
diff --git a/contracts/src/utils/mod.rs b/contracts/src/utils/mod.rs
index 14c8480..a0cbd23 100644
--- a/contracts/src/utils/mod.rs
+++ b/contracts/src/utils/mod.rs
@@ -1,5 +1,6 @@
 //! Common Smart Contracts utilities.
 
+pub mod math;
 pub mod structs;
 
 cfg_if::cfg_if! {


Test patch:
diff --git a/examples/erc20/tests/erc20.rs b/examples/erc20/tests/erc20.rs
index c7e73d2..059c94f 100644
--- a/examples/erc20/tests/erc20.rs
+++ b/examples/erc20/tests/erc20.rs
@@ -6,7 +6,7 @@ use alloy::{
     sol_types::{SolConstructor, SolError},
 };
 use alloy_primitives::uint;
-use e2e::{receipt, send, watch, EventExt, Panic, PanicCode, Revert, User};
+use e2e::{receipt, send, watch, Account, EventExt, Panic, PanicCode, Revert};
 use eyre::Result;
 
 use crate::abi::Erc20;
@@ -38,7 +38,7 @@ async fn deploy(
 // ============================================================================
 
 #[e2e::test]
-async fn constructs(alice: User) -> Result<()> {
+async fn constructs(alice: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract = Erc20::new(contract_addr, &alice.wallet);
 
@@ -58,7 +58,7 @@ async fn constructs(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn mints(alice: User) -> Result<()> {
+async fn mints(alice: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
@@ -90,7 +90,7 @@ async fn mints(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn mints_rejects_invalid_receiver(alice: User) -> Result<()> {
+async fn mints_rejects_invalid_receiver(alice: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract = Erc20::new(contract_addr, &alice.wallet);
     let invalid_receiver = Address::ZERO;
@@ -118,7 +118,7 @@ async fn mints_rejects_invalid_receiver(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn mints_rejects_overflow(alice: User) -> Result<()> {
+async fn mints_rejects_overflow(alice: Account) -> Result<()> {
     let max_cap = U256::MAX;
 
     let contract_addr = deploy(alice.url(), &alice.pk(), Some(max_cap)).await?;
@@ -153,7 +153,7 @@ async fn mints_rejects_overflow(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn transfers(alice: User, bob: User) -> Result<()> {
+async fn transfers(alice: Account, bob: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
@@ -191,8 +191,8 @@ async fn transfers(alice: User, bob: User) -> Result<()> {
 
 #[e2e::test]
 async fn transfer_rejects_insufficient_balance(
-    alice: User,
-    bob: User,
+    alice: Account,
+    bob: Account,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
@@ -234,7 +234,7 @@ async fn transfer_rejects_insufficient_balance(
 }
 
 #[e2e::test]
-async fn transfer_rejects_invalid_receiver(alice: User) -> Result<()> {
+async fn transfer_rejects_invalid_receiver(alice: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
@@ -273,7 +273,7 @@ async fn transfer_rejects_invalid_receiver(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn approves(alice: User, bob: User) -> Result<()> {
+async fn approves(alice: Account, bob: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
@@ -340,7 +340,7 @@ async fn approves(alice: User, bob: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn approve_rejects_invalid_spender(alice: User) -> Result<()> {
+async fn approve_rejects_invalid_spender(alice: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
@@ -390,7 +390,7 @@ async fn approve_rejects_invalid_spender(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn transfers_from(alice: User, bob: User) -> Result<()> {
+async fn transfers_from(alice: Account, bob: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let contract_bob = Erc20::new(contract_addr, &bob.wallet);
@@ -439,8 +439,8 @@ async fn transfers_from(alice: User, bob: User) -> Result<()> {
 
 #[e2e::test]
 async fn transfer_from_reverts_insufficient_balance(
-    alice: User,
-    bob: User,
+    alice: Account,
+    bob: Account,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
@@ -494,8 +494,8 @@ async fn transfer_from_reverts_insufficient_balance(
 
 #[e2e::test]
 async fn transfer_from_rejects_insufficient_allowance(
-    alice: User,
-    bob: User,
+    alice: Account,
+    bob: Account,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
@@ -549,8 +549,8 @@ async fn transfer_from_rejects_insufficient_allowance(
 
 #[e2e::test]
 async fn transfer_from_rejects_invalid_receiver(
-    alice: User,
-    bob: User,
+    alice: Account,
+    bob: Account,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
@@ -607,7 +607,7 @@ async fn transfer_from_rejects_invalid_receiver(
 // ============================================================================
 
 #[e2e::test]
-async fn burns(alice: User) -> Result<()> {
+async fn burns(alice: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
@@ -642,7 +642,7 @@ async fn burns(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn burn_rejects_insufficient_balance(alice: User) -> Result<()> {
+async fn burn_rejects_insufficient_balance(alice: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
@@ -677,7 +677,7 @@ async fn burn_rejects_insufficient_balance(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn burns_from(alice: User, bob: User) -> Result<()> {
+async fn burns_from(alice: Account, bob: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let contract_bob = Erc20::new(contract_addr, &bob.wallet);
@@ -729,8 +729,8 @@ async fn burns_from(alice: User, bob: User) -> Result<()> {
 
 #[e2e::test]
 async fn burn_from_reverts_insufficient_balance(
-    alice: User,
-    bob: User,
+    alice: Account,
+    bob: Account,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
@@ -784,8 +784,8 @@ async fn burn_from_reverts_insufficient_balance(
 
 #[e2e::test]
 async fn burn_from_rejects_insufficient_allowance(
-    alice: User,
-    bob: User,
+    alice: Account,
+    bob: Account,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
@@ -842,7 +842,7 @@ async fn burn_from_rejects_insufficient_allowance(
 // ============================================================================
 
 #[e2e::test]
-async fn mint_rejects_exceeding_cap(alice: User) -> Result<()> {
+async fn mint_rejects_exceeding_cap(alice: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
@@ -878,7 +878,7 @@ async fn mint_rejects_exceeding_cap(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn mint_rejects_when_cap_reached(alice: User) -> Result<()> {
+async fn mint_rejects_when_cap_reached(alice: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
     let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
@@ -913,7 +913,9 @@ async fn mint_rejects_when_cap_reached(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn should_not_deploy_capped_with_invalid_cap(alice: User) -> Result<()> {
+async fn should_not_deploy_capped_with_invalid_cap(
+    alice: Account,
+) -> Result<()> {
     let invalid_cap = U256::ZERO;
     let err = deploy(alice.url(), &alice.pk(), Some(invalid_cap))
         .await
diff --git a/examples/erc721/tests/erc721.rs b/examples/erc721/tests/erc721.rs
index 273022c..e78f9ee 100644
--- a/examples/erc721/tests/erc721.rs
+++ b/examples/erc721/tests/erc721.rs
@@ -6,7 +6,7 @@ use alloy::{
     sol_types::SolConstructor,
 };
 use alloy_primitives::uint;
-use e2e::{receipt, send, watch, EventExt, Revert, User};
+use e2e::{receipt, send, watch, Account, EventExt, Revert};
 
 use crate::abi::Erc721;
 
@@ -32,7 +32,7 @@ async fn deploy(rpc_url: &str, private_key: &str) -> eyre::Result<Address> {
 }
 
 #[e2e::test]
-async fn constructs(alice: User) -> eyre::Result<()> {
+async fn constructs(alice: Account) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
     let contract = Erc721::new(contract_addr, &alice.wallet);
 
@@ -45,7 +45,7 @@ async fn constructs(alice: User) -> eyre::Result<()> {
 }
 
 #[e2e::test]
-async fn mints(alice: User) -> eyre::Result<()> {
+async fn mints(alice: Account) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
     let contract = Erc721::new(contract_addr, &alice.wallet);
 
@@ -61,7 +61,7 @@ async fn mints(alice: User) -> eyre::Result<()> {
 }
 
 #[e2e::test]
-async fn errors_when_reusing_token_id(alice: User) -> eyre::Result<()> {
+async fn errors_when_reusing_token_id(alice: Account) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
     let contract = Erc721::new(contract_addr, &alice.wallet);
 
@@ -77,7 +77,7 @@ async fn errors_when_reusing_token_id(alice: User) -> eyre::Result<()> {
 }
 
 #[e2e::test]
-async fn transfers(alice: User, bob: User) -> eyre::Result<()> {
+async fn transfers(alice: Account, bob: Account) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
     let contract = Erc721::new(contract_addr, &alice.wallet);
 
@@ -102,8 +102,8 @@ async fn transfers(alice: User, bob: User) -> eyre::Result<()> {
 
 #[e2e::test]
 async fn errors_when_transfer_nonexistent_token(
-    alice: User,
-    bob: User,
+    alice: Account,
+    bob: Account,
 ) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
     let contract = Erc721::new(contract_addr, &alice.wallet);
@@ -120,7 +120,10 @@ async fn errors_when_transfer_nonexistent_token(
 }
 
 #[e2e::test]
-async fn approves_token_transfer(alice: User, bob: User) -> eyre::Result<()> {
+async fn approves_token_transfer(
+    alice: Account,
+    bob: Account,
+) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
     let contract = Erc721::new(contract_addr, &alice.wallet);
 
@@ -141,8 +144,8 @@ async fn approves_token_transfer(alice: User, bob: User) -> eyre::Result<()> {
 
 #[e2e::test]
 async fn errors_when_transfer_unapproved_token(
-    alice: User,
-    bob: User,
+    alice: Account,
+    bob: Account,
 ) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
     let contract = Erc721::new(contract_addr, &alice.wallet);
diff --git a/examples/ownable/tests/ownable.rs b/examples/ownable/tests/ownable.rs
index 52fc03a..207d701 100644
--- a/examples/ownable/tests/ownable.rs
+++ b/examples/ownable/tests/ownable.rs
@@ -8,7 +8,7 @@ use alloy::{
     sol,
     sol_types::{SolConstructor, SolError, SolEvent},
 };
-use e2e::{receipt, send, EventExt, Revert, User};
+use e2e::{receipt, send, Account, EventExt, Revert};
 use eyre::Result;
 
 use crate::abi::Ownable;
@@ -17,10 +17,10 @@ mod abi;
 
 sol!("src/constructor.sol");
 
-async fn deploy(user: &User, owner: Address) -> eyre::Result<Address> {
+async fn deploy(account: &Account, owner: Address) -> eyre::Result<Address> {
     let args = OwnableExample::constructorCall { initialOwner: owner };
     let args = alloy::hex::encode(args.abi_encode());
-    e2e::deploy(user.url(), &user.pk(), Some(args)).await
+    e2e::deploy(account.url(), &account.pk(), Some(args)).await
 }
 
 // ============================================================================
@@ -28,7 +28,7 @@ async fn deploy(user: &User, owner: Address) -> eyre::Result<Address> {
 // ============================================================================
 
 #[e2e::test]
-async fn constructs(alice: User) -> Result<()> {
+async fn constructs(alice: Account) -> Result<()> {
     let alice_addr = alice.address();
     let contract_addr = deploy(&alice, alice_addr).await?;
     let contract = Ownable::new(contract_addr, &alice.wallet);
@@ -41,7 +41,7 @@ async fn constructs(alice: User) -> Result<()> {
 
 #[e2e::test]
 async fn emits_ownership_transfer_during_construction(
-    alice: User,
+    alice: Account,
 ) -> Result<()> {
     let alice_addr = alice.address();
 
@@ -64,7 +64,7 @@ async fn emits_ownership_transfer_during_construction(
 }
 
 #[e2e::test]
-async fn rejects_zero_address_initial_owner(alice: User) -> Result<()> {
+async fn rejects_zero_address_initial_owner(alice: Account) -> Result<()> {
     let err = deploy(&alice, Address::ZERO)
         .await
         .expect_err("should not deploy due to `OwnableInvalidOwner`");
@@ -81,7 +81,7 @@ async fn rejects_zero_address_initial_owner(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn transfers_ownership(alice: User, bob: User) -> Result<()> {
+async fn transfers_ownership(alice: Account, bob: Account) -> Result<()> {
     let alice_addr = alice.address();
     let bob_addr = bob.address();
 
@@ -102,8 +102,8 @@ async fn transfers_ownership(alice: User, bob: User) -> Result<()> {
 
 #[e2e::test]
 async fn prevents_non_owners_from_transferring(
-    alice: User,
-    bob: User,
+    alice: Account,
+    bob: Account,
 ) -> Result<()> {
     let alice_addr = alice.address();
     let bob_addr = bob.address();
@@ -121,7 +121,7 @@ async fn prevents_non_owners_from_transferring(
 }
 
 #[e2e::test]
-async fn guards_against_stuck_state(alice: User) -> Result<()> {
+async fn guards_against_stuck_state(alice: Account) -> Result<()> {
     let alice_addr = alice.address();
     let contract_addr = deploy(&alice, alice_addr).await?;
     let contract = Ownable::new(contract_addr, &alice.wallet);
@@ -137,7 +137,7 @@ async fn guards_against_stuck_state(alice: User) -> Result<()> {
 }
 
 #[e2e::test]
-async fn loses_ownership_after_renouncement(alice: User) -> Result<()> {
+async fn loses_ownership_after_renouncement(alice: Account) -> Result<()> {
     let alice_addr = alice.address();
     let contract_addr = deploy(&alice, alice_addr).await?;
     let contract = Ownable::new(contract_addr, &alice.wallet);
@@ -156,8 +156,8 @@ async fn loses_ownership_after_renouncement(alice: User) -> Result<()> {
 
 #[e2e::test]
 async fn prevents_non_owners_from_renouncement(
-    alice: User,
-    bob: User,
+    alice: Account,
+    bob: Account,
 ) -> Result<()> {
     let alice_addr = alice.address();
     let bob_addr = bob.address();
diff --git a/lib/e2e-proc/src/lib.rs b/lib/e2e-proc/src/lib.rs
index 739bf86..f0f7bae 100644
--- a/lib/e2e-proc/src/lib.rs
+++ b/lib/e2e-proc/src/lib.rs
@@ -3,15 +3,15 @@ use proc_macro::TokenStream;
 
 mod test;
 
-/// Defines an end-to-end Stylus contract test that sets up `e2e::User`s based
-/// on the function's parameters.
+/// Defines an end-to-end Stylus contract test that sets up `e2e::Account`s
+/// based on the function's parameters.
 ///
 /// # Examples
 ///
 /// ```rust,ignore
 /// #[e2e::test]
-/// async fn foo(alice: User, bob: User) -> eyre::Result<()> {
-///     let charlie = User::new().await?;
+/// async fn foo(alice: Account, bob: Account) -> eyre::Result<()> {
+///     let charlie = Account::new().await?;
 ///     // ...
 /// }
 /// ```
diff --git a/lib/e2e-proc/src/test.rs b/lib/e2e-proc/src/test.rs
index f469067..f981d89 100644
--- a/lib/e2e-proc/src/test.rs
+++ b/lib/e2e-proc/src/test.rs
@@ -13,7 +13,7 @@ macro_rules! error {
     }};
 }
 
-/// Defines an end-to-end test that injects test users through parameters.
+/// Defines an end-to-end test that injects test accounts through parameters.
 ///
 /// For more information see [`crate::test`].
 pub(crate) fn test(_attr: &TokenStream, input: TokenStream) -> TokenStream {
@@ -25,21 +25,21 @@ pub(crate) fn test(_attr: &TokenStream, input: TokenStream) -> TokenStream {
     let fn_stmts = &item_fn.block.stmts;
     let fn_args = &sig.inputs;
 
-    let user_declarations = fn_args.into_iter().map(|arg| {
+    let account_declarations = fn_args.into_iter().map(|arg| {
         let FnArg::Typed(arg) = arg else {
             error!(arg, "unexpected receiver argument in test signature");
         };
-        let user_arg_binding = &arg.pat;
-        let user_ty = &arg.ty;
+        let account_arg_binding = &arg.pat;
+        let account_ty = &arg.ty;
         quote! {
-            let #user_arg_binding = #user_ty::new().await?;
+            let #account_arg_binding = #account_ty::new().await?;
         }
     });
     quote! {
         #( #attrs )*
         #[tokio::test]
         async fn #fn_name() #fn_return_type {
-            #( #user_declarations )*
+            #( #account_declarations )*
             #( #fn_stmts )*
         }
     }
diff --git a/lib/e2e/README.md b/lib/e2e/README.md
index 241f378..2935e12 100644
--- a/lib/e2e/README.md
+++ b/lib/e2e/README.md
@@ -15,14 +15,14 @@ tests suite using the `e2e` crate.
 
 [GitHub workflow]: ../../.github/workflows/e2e-tests.yml
 
-### Users
+### Accounts
 
 Decorate your tests with the `test` procedural macro: a thin wrapper over
-`tokio::test` that sets up `User`s for your test.
+`tokio::test` that sets up `Account`s for your test.
 
 ```rust,ignore
 #[e2e::test]
-async fn users_are_funded(alice: User) -> eyre::Result<()> {
+async fn accounts_are_funded(alice: Account) -> eyre::Result<()> {
     let balance = alice.wallet.get_balance(alice.address()).await?;
     let expected = parse_ether("10")?;
     assert_eq!(expected, balance);
@@ -30,18 +30,18 @@ async fn users_are_funded(alice: User) -> eyre::Result<()> {
 }
 ```
 
-A `User` is a thin wrapper over a [`PrivateKeySigner`] and an `alloy` provider with a
+A `Account` is a thin wrapper over a [`PrivateKeySigner`] and an `alloy` provider with a
 [`WalletFiller`]. Both of them are connected to the RPC endpoint defined by the
-`RPC_URL` environment variable. This means that a `User` is the main proxy
+`RPC_URL` environment variable. This means that a `Account` is the main proxy
 between the RPC and the test code.
 
-All users start with 10 ETH as balance. You can have multiple users as
-parameters of your test function, or you can create new users separately:
+All accounts start with 10 ETH as balance. You can have multiple accounts as
+parameters of your test function, or you can create new accounts separately:
 
 ```rust,ignore
 #[e2e::test]
-async fn foo(alice: User, bob: User) -> eyre::Result<()> {
-    let charlie = User::new().await?;
+async fn foo(alice: Account, bob: Account) -> eyre::Result<()> {
+    let charlie = Account::new().await?;
     // ...
 }
 ```
@@ -98,7 +98,7 @@ You can then make calls to your contract:
 
 ```rust,ignore
 #[e2e::test]
-async fn constructs(alice: User) -> Result<()> {
+async fn constructs(alice: Account) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
     let contract = Erc20::new(contract_addr, &alice.wallet);
 
diff --git a/lib/e2e/src/user.rs b/lib/e2e/src/account.rs
similarity index 74%
rename from lib/e2e/src/user.rs
rename to lib/e2e/src/account.rs
index 5677126..e1cdb8c 100644
--- a/lib/e2e/src/user.rs
+++ b/lib/e2e/src/account.rs
@@ -13,26 +13,26 @@ use crate::{
     system::{Wallet, RPC_URL_ENV_VAR_NAME},
 };
 
-/// Type that corresponds to a test user.
+/// Type that corresponds to a test account.
 #[derive(Clone, Debug)]
-pub struct User {
+pub struct Account {
     /// The account's local private key wrapper.
     pub signer: PrivateKeySigner,
     /// The account's wallet -- an `alloy` provider with a `WalletFiller`.
     pub wallet: Wallet,
 }
 
-impl User {
-    /// Create a new user.
+impl Account {
+    /// Create a new account.
     ///
     /// # Errors
     ///
     /// May fail if funding the newly created account fails.
     pub async fn new() -> Result<Self> {
-        UserFactory::create().await
+        AccountFactory::create().await
     }
 
-    /// Get a hex-encoded String representing this user's private key.
+    /// Get a hex-encoded String representing this account's private key.
     #[must_use]
     pub fn pk(&self) -> String {
         alloy::hex::encode(self.signer.to_bytes())
@@ -44,26 +44,27 @@ impl User {
         self.signer.address()
     }
 
-    /// The rpc endpoint this user's provider is connect to.
+    /// The rpc endpoint this account's provider is connect to.
     #[must_use]
     pub fn url(&self) -> &str {
         self.wallet.client().transport().url()
     }
 }
 
-/// A unit struct used as a synchronization mechanism in [`SYNC_USER_FACTORY`].
-struct UserFactory;
+/// A unit struct used as a synchronization mechanism in
+/// [`SYNC_ACCOUNT_FACTORY`].
+struct AccountFactory;
 
-impl UserFactory {
+impl AccountFactory {
     /// Get access to the factory in a synchronized manner.
     async fn lock() -> MutexGuard<'static, Self> {
-        /// Since after wallet generation users get funded in the nitro test
-        /// node from a single "god" wallet, we must synchronize user
+        /// Since after wallet generation accounts get funded in the nitro test
+        /// node from a single "god" wallet, we must synchronize account
         /// creation (otherwise the nonce will be too low).
-        static SYNC_USER_FACTORY: Lazy<Mutex<UserFactory>> =
-            Lazy::new(|| Mutex::new(UserFactory));
+        static SYNC_ACCOUNT_FACTORY: Lazy<Mutex<AccountFactory>> =
+            Lazy::new(|| Mutex::new(AccountFactory));
 
-        SYNC_USER_FACTORY.lock().await
+        SYNC_ACCOUNT_FACTORY.lock().await
     }
 
     /// Create new account and fund it via nitro test node access.
@@ -72,8 +73,8 @@ impl UserFactory {
     ///
     /// May fail if unable to find the path to the node or if funding the newly
     /// created account fails.
-    async fn create() -> eyre::Result<User> {
-        let _lock = UserFactory::lock().await;
+    async fn create() -> eyre::Result<Account> {
+        let _lock = AccountFactory::lock().await;
 
         let signer = PrivateKeySigner::random();
         let addr = signer.address();
@@ -100,10 +101,10 @@ impl UserFactory {
             .on_http(rpc_url);
 
         if output.status.success() {
-            Ok(User { signer, wallet })
+            Ok(Account { signer, wallet })
         } else {
             let err = String::from_utf8_lossy(&output.stderr);
-            bail!("user's wallet wasn't funded - address is {addr}:\n{err}")
+            bail!("account's wallet wasn't funded - address is {addr}:\n{err}")
         }
     }
 }
diff --git a/lib/e2e/src/lib.rs b/lib/e2e/src/lib.rs
index 533e7c8..8b3a33f 100644
--- a/lib/e2e/src/lib.rs
+++ b/lib/e2e/src/lib.rs
@@ -1,18 +1,18 @@
 #![doc = include_str!("../README.md")]
+mod account;
 mod deploy;
 mod environment;
 mod error;
 mod event;
 mod project;
 mod system;
-mod user;
 
+pub use account::Account;
 pub use deploy::deploy;
 pub use e2e_proc::test;
 pub use error::{Panic, PanicCode, Revert};
 pub use event::EventExt;
 pub use system::{provider, Provider, Wallet};
-pub use user::User;
 
 /// This macro provides a shorthand for broadcasting the transaction to the
 /// network.
@@ -23,7 +23,7 @@ pub use user::User;
 ///
 /// ```rust,ignore
 /// #[e2e::test]
-/// async fn foo(alice: User) -> eyre::Result<()> {
+/// async fn foo(alice: Account) -> eyre::Result<()> {
 ///     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
 ///     let contract = Erc721::new(contract_addr, &alice.wallet);
 ///
@@ -48,7 +48,7 @@ macro_rules! send {
 ///
 /// ```rust,ignore
 /// #[e2e::test]
-/// async fn foo(alice: User) -> eyre::Result<()> {
+/// async fn foo(alice: Account) -> eyre::Result<()> {
 ///     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
 ///     let contract = Erc721::new(contract_addr, &alice.wallet);
 ///
@@ -74,7 +74,7 @@ macro_rules! watch {
 ///
 /// ```rust,ignore
 /// #[e2e::test]
-/// async fn foo(alice: User) -> eyre::Result<()> {
+/// async fn foo(alice: Account) -> eyre::Result<()> {
 ///     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
 ///     let contract = Erc721::new(contract_addr, &alice.wallet);
 ///