Repository: OpenZeppelin/rust-contracts-stylus
Pull request #143 (2024-06-21 09:47:17)

chore: enforce uint macro for const evaluation
Curious why we are not using const evaluation for alloy number types wherever is possible?


netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *contracts-stylus* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 6150b39051ff1220fcfd3c3687aed857b6364128 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/contracts-stylus/deploys/66754c27e477b40008b73985 |
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/143?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.2%. Comparing base [(`d431f9a`)](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/commit/d431f9a79452a880833e3342008cb330e1a7ad7c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin) to head [(`6150b39`)](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/commit/6150b39051ff1220fcfd3c3687aed857b6364128?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin).

<details><summary>Additional details and impacted files</summary>


| [Files](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/143?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin) | Coverage Δ | |
|---|---|---|
| [contracts/src/token/erc20/extensions/burnable.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/143?src=pr&el=tree&filepath=contracts%2Fsrc%2Ftoken%2Ferc20%2Fextensions%2Fburnable.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-Y29udHJhY3RzL3NyYy90b2tlbi9lcmMyMC9leHRlbnNpb25zL2J1cm5hYmxlLnJz) | `100.0% <100.0%> (ø)` | |
| [contracts/src/token/erc20/extensions/capped.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/143?src=pr&el=tree&filepath=contracts%2Fsrc%2Ftoken%2Ferc20%2Fextensions%2Fcapped.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-Y29udHJhY3RzL3NyYy90b2tlbi9lcmMyMC9leHRlbnNpb25zL2NhcHBlZC5ycw==) | `54.5% <100.0%> (ø)` | |
| [contracts/src/token/erc20/mod.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/143?src=pr&el=tree&filepath=contracts%2Fsrc%2Ftoken%2Ferc20%2Fmod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-Y29udHJhY3RzL3NyYy90b2tlbi9lcmMyMC9tb2QucnM=) | `95.7% <100.0%> (ø)` | |
| [contracts/src/token/erc721/extensions/burnable.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/143?src=pr&el=tree&filepath=contracts%2Fsrc%2Ftoken%2Ferc721%2Fextensions%2Fburnable.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-Y29udHJhY3RzL3NyYy90b2tlbi9lcmM3MjEvZXh0ZW5zaW9ucy9idXJuYWJsZS5ycw==) | `100.0% <100.0%> (ø)` | |
| [...ontracts/src/token/erc721/extensions/enumerable.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/143?src=pr&el=tree&filepath=contracts%2Fsrc%2Ftoken%2Ferc721%2Fextensions%2Fenumerable.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-Y29udHJhY3RzL3NyYy90b2tlbi9lcmM3MjEvZXh0ZW5zaW9ucy9lbnVtZXJhYmxlLnJz) | `87.6% <100.0%> (ø)` | |
| [contracts/src/token/erc721/mod.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/143?src=pr&el=tree&filepath=contracts%2Fsrc%2Ftoken%2Ferc721%2Fmod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-Y29udHJhY3RzL3NyYy90b2tlbi9lcmM3MjEvbW9kLnJz) | `64.6% <100.0%> (ø)` | |
| [contracts/src/utils/structs/bitmap.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/143?src=pr&el=tree&filepath=contracts%2Fsrc%2Futils%2Fstructs%2Fbitmap.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-Y29udHJhY3RzL3NyYy91dGlscy9zdHJ1Y3RzL2JpdG1hcC5ycw==) | `84.0% <ø> (ø)` | |

</details>

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 69335e3..58db777 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2627,11 +2627,15 @@ dependencies = [
 name = "ownable-example"
 version = "0.0.0"
 dependencies = [
+ "alloy",
  "alloy-primitives 0.3.3",
+ "e2e",
+ "eyre",
  "mini-alloc",
  "openzeppelin-stylus",
  "stylus-proc",
  "stylus-sdk",
+ "tokio",
 ]
 
 [[package]]
diff --git a/README.md b/README.md
index 7f0a40e..b45a9c9 100644
--- a/README.md
+++ b/README.md
@@ -14,10 +14,12 @@
   library.
 - First-class `no_std` support.
 - Solidity constructors powered by [`koba`].
-- Unit and integration test affordances, used in our own tests.
+- [Unit] and [integration] test affordances, used in our own tests.
 
 [`openzeppelin-contracts`]: https://github.com/OpenZeppelin/openzeppelin-contracts
 [`koba`]: https://github.com/OpenZeppelin/koba
+[Unit]: ./lib/motsu/README.md
+[integration]: ./lib/e2e/README.md
 
 ## Usage
 
diff --git a/contracts/src/access/ownable.rs b/contracts/src/access/ownable.rs
index fed86eb..3914568 100644
--- a/contracts/src/access/ownable.rs
+++ b/contracts/src/access/ownable.rs
@@ -142,22 +142,13 @@ impl Ownable {
 
 #[cfg(all(test, feature = "std"))]
 mod tests {
-    use alloy_primitives::{address, Address, U256};
-    use stylus_sdk::{
-        msg,
-        storage::{StorageAddress, StorageType},
-    };
+    use alloy_primitives::{address, Address};
+    use stylus_sdk::msg;
 
     use super::{Error, Ownable};
 
     const ALICE: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
 
-    #[motsu::test]
-    fn rejects_zero_address_initial_owner(contract: Ownable) {
-        // FIXME: Once constructors are supported this check should fail.
-        assert_eq!(contract._owner.get(), Address::ZERO);
-    }
-
     #[motsu::test]
     fn reads_owner(contract: Ownable) {
         contract._owner.set(msg::sender());
diff --git a/contracts/src/token/erc20/extensions/burnable.rs b/contracts/src/token/erc20/extensions/burnable.rs
index 770da0d..3d36c3c 100644
--- a/contracts/src/token/erc20/extensions/burnable.rs
+++ b/contracts/src/token/erc20/extensions/burnable.rs
@@ -71,7 +71,7 @@ impl IErc20Burnable for Erc20 {
 
 #[cfg(all(test, feature = "std"))]
 mod tests {
-    use alloy_primitives::{address, Address, U256};
+    use alloy_primitives::{address, uint, Address, U256};
     use stylus_sdk::msg;
 
     use super::IErc20Burnable;
@@ -80,14 +80,14 @@ mod tests {
     #[motsu::test]
     fn burns(contract: Erc20) {
         let zero = U256::ZERO;
-        let one = U256::from(1);
+        let one = uint!(1_U256);
 
         assert_eq!(zero, contract.total_supply());
 
         // Mint some tokens for msg::sender().
         let sender = msg::sender();
 
-        let two = U256::from(2);
+        let two = uint!(2_U256);
         contract._update(Address::ZERO, sender, two).unwrap();
         assert_eq!(two, contract.balance_of(sender));
         assert_eq!(two, contract.total_supply());
@@ -101,7 +101,7 @@ mod tests {
     #[motsu::test]
     fn burns_errors_when_insufficient_balance(contract: Erc20) {
         let zero = U256::ZERO;
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         let sender = msg::sender();
 
         assert_eq!(zero, contract.balance_of(sender));
@@ -116,11 +116,11 @@ mod tests {
         let sender = msg::sender();
 
         // Alice approves `msg::sender`.
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract._allowances.setter(alice).setter(sender).set(one);
 
         // Mint some tokens for Alice.
-        let two = U256::from(2);
+        let two = uint!(2_U256);
         contract._update(Address::ZERO, alice, two).unwrap();
         assert_eq!(two, contract.balance_of(alice));
         assert_eq!(two, contract.total_supply());
@@ -138,12 +138,12 @@ mod tests {
 
         // Alice approves `msg::sender`.
         let zero = U256::ZERO;
-        let one = U256::from(1);
+        let one = uint!(1_U256);
 
         contract._allowances.setter(alice).setter(msg::sender()).set(one);
         assert_eq!(zero, contract.balance_of(alice));
 
-        let one = U256::from(1);
+        let one = uint!(1_U256);
 
         let result = contract.burn_from(alice, one);
         assert!(matches!(result, Err(Error::InsufficientBalance(_))));
@@ -151,7 +151,7 @@ mod tests {
 
     #[motsu::test]
     fn burns_from_errors_when_invalid_sender(contract: Erc20) {
-        let one = U256::from(1);
+        let one = uint!(1_U256);
 
         contract
             ._allowances
@@ -168,7 +168,7 @@ mod tests {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
 
         // Mint some tokens for Alice.
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract._update(Address::ZERO, alice, one).unwrap();
         assert_eq!(one, contract.balance_of(alice));
 
diff --git a/contracts/src/token/erc20/extensions/capped.rs b/contracts/src/token/erc20/extensions/capped.rs
index 175aa17..5e69fbe 100644
--- a/contracts/src/token/erc20/extensions/capped.rs
+++ b/contracts/src/token/erc20/extensions/capped.rs
@@ -54,18 +54,18 @@ impl Capped {
 
 #[cfg(all(test, feature = "std"))]
 mod tests {
-    use alloy_primitives::U256;
+    use alloy_primitives::{uint, U256};
     use stylus_sdk::storage::{StorageType, StorageU256};
 
     use super::Capped;
 
     #[motsu::test]
     fn cap_works(contract: Capped) {
-        let value = U256::from(2024);
+        let value = uint!(2024_U256);
         contract._cap.set(value);
         assert_eq!(contract.cap(), value);
 
-        let value = U256::from(1);
+        let value = uint!(1_U256);
         contract._cap.set(value);
         assert_eq!(contract.cap(), value);
     }
diff --git a/contracts/src/token/erc20/mod.rs b/contracts/src/token/erc20/mod.rs
index d3c1903..984b465 100644
--- a/contracts/src/token/erc20/mod.rs
+++ b/contracts/src/token/erc20/mod.rs
@@ -498,11 +498,8 @@ impl Erc20 {
 
 #[cfg(all(test, feature = "std"))]
 mod tests {
-    use alloy_primitives::{address, Address, U256};
-    use stylus_sdk::{
-        msg,
-        storage::{StorageMap, StorageType, StorageU256},
-    };
+    use alloy_primitives::{address, uint, Address, U256};
+    use stylus_sdk::{msg, storage::StorageType};
 
     use super::{Erc20, Error, IErc20};
 
@@ -512,7 +509,7 @@ mod tests {
         assert_eq!(U256::ZERO, balance);
 
         let owner = msg::sender();
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract._balances.setter(owner).set(one);
         let balance = contract.balance_of(owner);
         assert_eq!(one, balance);
@@ -521,7 +518,7 @@ mod tests {
     #[motsu::test]
     fn update_mint(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
-        let one = U256::from(1);
+        let one = uint!(1_U256);
 
         // Store initial balance & supply.
         let initial_balance = contract.balance_of(alice);
@@ -540,7 +537,7 @@ mod tests {
     #[should_panic = "should not exceed `U256::MAX` for `_total_supply`"]
     fn update_mint_errors_arithmetic_overflow(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         assert_eq!(U256::ZERO, contract.balance_of(alice));
         assert_eq!(U256::ZERO, contract.total_supply());
 
@@ -557,7 +554,7 @@ mod tests {
     #[motsu::test]
     fn mint_works(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
-        let one = U256::from(1);
+        let one = uint!(1_U256);
 
         // Store initial balance & supply.
         let initial_balance = contract.balance_of(alice);
@@ -575,7 +572,7 @@ mod tests {
     #[motsu::test]
     fn mint_errors_invalid_receiver(contract: Erc20) {
         let receiver = Address::ZERO;
-        let one = U256::from(1);
+        let one = uint!(1_U256);
 
         // Store initial balance & supply.
         let initial_balance = contract.balance_of(receiver);
@@ -594,7 +591,7 @@ mod tests {
     #[should_panic = "should not exceed `U256::MAX` for `_total_supply`"]
     fn mint_errors_arithmetic_overflow(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         assert_eq!(U256::ZERO, contract.balance_of(alice));
         assert_eq!(U256::ZERO, contract.total_supply());
 
@@ -610,8 +607,8 @@ mod tests {
     #[motsu::test]
     fn update_burn(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
-        let one = U256::from(1);
-        let two = U256::from(2);
+        let one = uint!(1_U256);
+        let two = uint!(2_U256);
 
         // Initialize state for the test case:
         // Alice's balance as `two`.
@@ -635,8 +632,8 @@ mod tests {
     #[motsu::test]
     fn update_burn_errors_insufficient_balance(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
-        let one = U256::from(1);
-        let two = U256::from(2);
+        let one = uint!(1_U256);
+        let two = uint!(2_U256);
 
         // Initialize state for the test case:
         // Alice's balance as `one`.
@@ -661,7 +658,7 @@ mod tests {
     fn update_transfer(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
         let bob = address!("B0B0cB49ec2e96DF5F5fFB081acaE66A2cBBc2e2");
-        let one = U256::from(1);
+        let one = uint!(1_U256);
 
         // Initialize state for the test case:
         //  Alice's & Bob's balance as `one`.
@@ -689,7 +686,7 @@ mod tests {
     fn update_transfer_errors_insufficient_balance(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
         let bob = address!("B0B0cB49ec2e96DF5F5fFB081acaE66A2cBBc2e2");
-        let one = U256::from(1);
+        let one = uint!(1_U256);
 
         // Initialize state for the test case:
         // Alice's & Bob's balance as `one`.
@@ -719,11 +716,11 @@ mod tests {
         let bob = address!("B0B0cB49ec2e96DF5F5fFB081acaE66A2cBBc2e2");
 
         // Alice approves `msg::sender`.
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract._allowances.setter(alice).setter(msg::sender()).set(one);
 
         // Mint some tokens for Alice.
-        let two = U256::from(2);
+        let two = uint!(2_U256);
         contract._update(Address::ZERO, alice, two).unwrap();
         assert_eq!(two, contract.balance_of(alice));
 
@@ -740,11 +737,11 @@ mod tests {
         let sender = msg::sender();
 
         // Alice approves `msg::sender`.
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract._allowances.setter(alice).setter(sender).set(one);
 
         // Mint some tokens for Alice.
-        let two = U256::from(2);
+        let two = uint!(2_U256);
         contract._update(Address::ZERO, alice, two).unwrap();
         assert_eq!(two, contract.balance_of(alice));
 
@@ -761,11 +758,11 @@ mod tests {
         let bob = address!("B0B0cB49ec2e96DF5F5fFB081acaE66A2cBBc2e2");
 
         // Alice approves `msg::sender`.
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract._allowances.setter(alice).setter(msg::sender()).set(one);
         assert_eq!(U256::ZERO, contract.balance_of(alice));
 
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         let result = contract.transfer_from(alice, bob, one);
         assert!(matches!(result, Err(Error::InsufficientBalance(_))));
     }
@@ -773,7 +770,7 @@ mod tests {
     #[motsu::test]
     fn transfer_from_errors_when_invalid_sender(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract
             ._allowances
             .setter(Address::ZERO)
@@ -786,7 +783,7 @@ mod tests {
     #[motsu::test]
     fn transfer_from_errors_when_invalid_receiver(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract._allowances.setter(alice).setter(msg::sender()).set(one);
         let result = contract.transfer_from(alice, Address::ZERO, one);
         assert!(matches!(result, Err(Error::InvalidReceiver(_))));
@@ -798,7 +795,7 @@ mod tests {
         let bob = address!("B0B0cB49ec2e96DF5F5fFB081acaE66A2cBBc2e2");
 
         // Mint some tokens for Alice.
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract._update(Address::ZERO, alice, one).unwrap();
         assert_eq!(one, contract.balance_of(alice));
 
@@ -814,7 +811,7 @@ mod tests {
         let allowance = contract.allowance(owner, alice);
         assert_eq!(U256::ZERO, allowance);
 
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract._allowances.setter(owner).setter(alice).set(one);
         let allowance = contract.allowance(owner, alice);
         assert_eq!(one, allowance);
@@ -825,7 +822,7 @@ mod tests {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
 
         // `msg::sender` approves Alice.
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         contract.approve(alice, one).unwrap();
         assert_eq!(one, contract._allowances.get(msg::sender()).get(alice));
     }
@@ -833,7 +830,7 @@ mod tests {
     #[motsu::test]
     fn approve_errors_when_invalid_spender(contract: Erc20) {
         // `msg::sender` approves `Address::ZERO`.
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         let result = contract.approve(Address::ZERO, one);
         assert!(matches!(result, Err(Error::InvalidSpender(_))));
     }
diff --git a/contracts/src/token/erc721/extensions/burnable.rs b/contracts/src/token/erc721/extensions/burnable.rs
index 99d1604..ae43362 100644
--- a/contracts/src/token/erc721/extensions/burnable.rs
+++ b/contracts/src/token/erc721/extensions/burnable.rs
@@ -47,7 +47,7 @@ impl IErc721Burnable for Erc721 {
 
 #[cfg(all(test, feature = "std"))]
 mod tests {
-    use alloy_primitives::{address, Address, U256};
+    use alloy_primitives::{address, uint, Address, U256};
     use stylus_sdk::msg;
 
     use super::IErc721Burnable;
@@ -60,7 +60,7 @@ mod tests {
     #[motsu::test]
     fn burns(contract: Erc721) {
         let alice = msg::sender();
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         let token_id = random_token_id();
 
         contract._mint(alice, token_id).expect("should mint a token for Alice");
@@ -118,7 +118,7 @@ mod tests {
 
         contract._mint(alice, token_id).expect("should mint a token for Alice");
 
-        let err = contract.burn(token_id + U256::from(1)).unwrap_err();
+        let err = contract.burn(token_id + uint!(1_U256)).unwrap_err();
 
         assert!(matches!(err, Error::NonexistentToken(_)));
     }
diff --git a/contracts/src/token/erc721/extensions/enumerable.rs b/contracts/src/token/erc721/extensions/enumerable.rs
index 2ff98b7..edf290d 100644
--- a/contracts/src/token/erc721/extensions/enumerable.rs
+++ b/contracts/src/token/erc721/extensions/enumerable.rs
@@ -7,7 +7,7 @@
 //! CAUTION: [`Erc721`] extensions that implement custom
 //! [`Erc721::balance_of`] logic, such as [`Erc721Consecutive`], interfere with
 //! enumerability and should not be used together with [`Erc721Enumerable`].
-use alloy_primitives::{Address, U256};
+use alloy_primitives::{uint, Address, U256};
 use alloy_sol_types::sol;
 use stylus_proc::{external, sol_storage, SolidityError};
 
@@ -157,7 +157,7 @@ impl Erc721Enumerable {
         token_id: U256,
         erc721: &impl IErc721,
     ) -> Result<(), crate::token::erc721::Error> {
-        let length = erc721.balance_of(to)? - U256::from(1);
+        let length = erc721.balance_of(to)? - uint!(1_U256);
         self._owned_tokens.setter(to).setter(length).set(token_id);
         self._owned_tokens_index.setter(token_id).set(length);
 
@@ -308,7 +308,7 @@ impl Erc721Enumerable {
 
 #[cfg(all(test, feature = "std"))]
 mod tests {
-    use alloy_primitives::{address, Address, U256};
+    use alloy_primitives::{address, uint, Address, U256};
     use stylus_sdk::{
         msg,
         prelude::StorageType,
@@ -331,7 +331,7 @@ mod tests {
     ) {
         assert_eq!(U256::ZERO, contract.total_supply());
 
-        let token_idx = U256::from(2024);
+        let token_idx = uint!(2024_U256);
 
         let err = contract.token_by_index(token_idx).unwrap_err();
         assert!(matches!(err, Error::OutOfBoundsIndex(_)));
@@ -473,7 +473,7 @@ mod tests {
         assert!(res.is_ok());
 
         let err =
-            contract.token_of_owner_by_index(alice, U256::from(1)).unwrap_err();
+            contract.token_of_owner_by_index(alice, uint!(1_U256)).unwrap_err();
         assert!(matches!(err, Error::OutOfBoundsIndex(_)));
     }
 
diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs
index fcea565..f251185 100644
--- a/contracts/src/token/erc721/mod.rs
+++ b/contracts/src/token/erc721/mod.rs
@@ -1,7 +1,7 @@
 //! Implementation of the [`Erc721`] token standard.
 use alloc::vec;
 
-use alloy_primitives::{fixed_bytes, Address, FixedBytes, U128, U256};
+use alloy_primitives::{fixed_bytes, uint, Address, FixedBytes, U128, U256};
 use stylus_sdk::{
     abi::Bytes, alloy_sol_types::sol, call::Call, evm, msg, prelude::*,
 };
@@ -695,11 +695,11 @@ impl Erc721 {
             // Clear approval. No need to re-authorize or emit the `Approval`
             // event.
             self._approve(Address::ZERO, token_id, Address::ZERO, false)?;
-            self._balances.setter(from).sub_assign_unchecked(U256::from(1));
+            self._balances.setter(from).sub_assign_unchecked(uint!(1_U256));
         }
 
         if !to.is_zero() {
-            self._balances.setter(to).add_assign_unchecked(U256::from(1));
+            self._balances.setter(to).add_assign_unchecked(uint!(1_U256));
         }
 
         self._owners.setter(token_id).set(to);
@@ -1110,7 +1110,7 @@ impl Erc721 {
 
 #[cfg(all(test, feature = "std"))]
 mod tests {
-    use alloy_primitives::{address, Address, U256};
+    use alloy_primitives::{address, uint, Address, U256};
     use stylus_sdk::{msg, prelude::StorageType, storage::StorageMap};
 
     use super::{
@@ -1138,7 +1138,7 @@ mod tests {
         let balance = contract
             .balance_of(alice)
             .expect("should return the balance of Alice");
-        let one = U256::from(1);
+        let one = uint!(1_U256);
         assert!(balance >= one);
     }
 
diff --git a/contracts/src/utils/structs/bitmap.rs b/contracts/src/utils/structs/bitmap.rs
index 0bb8860..ccbb049 100644
--- a/contracts/src/utils/structs/bitmap.rs
+++ b/contracts/src/utils/structs/bitmap.rs
@@ -13,11 +13,11 @@
 //! - Accessing the same warm slot for every 256 _sequential_ indices
 //!
 //! [merkle-distributor]: https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol
-use alloy_primitives::U256;
+use alloy_primitives::{uint, U256};
 use stylus_proc::sol_storage;
 
-const ONE: U256 = U256::from_limbs([1, 0, 0, 0]);
-const HEX_FF: U256 = U256::from_limbs([255, 0, 0, 0]);
+const ONE: U256 = uint!(0x1_U256);
+const HEX_FF: U256 = uint!(0xff_U256);
 
 sol_storage! {
     /// State of bit map.
diff --git a/examples/access-control/Cargo.toml b/examples/access-control/Cargo.toml
index c795cf3..18316cb 100644
--- a/examples/access-control/Cargo.toml
+++ b/examples/access-control/Cargo.toml
@@ -7,7 +7,10 @@ publish = false
 version = "0.0.0"
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts", features = ["control", "erc20"] }
+openzeppelin-stylus = { path = "../../contracts", features = [
+  "control",
+  "erc20",
+] }
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -16,5 +19,3 @@ mini-alloc.workspace = true
 [lib]
 crate-type = ["lib", "cdylib"]
 
-[lints]
-workspace = true
\ No newline at end of file
diff --git a/examples/erc20/Cargo.toml b/examples/erc20/Cargo.toml
index d1351db..a1cc559 100644
--- a/examples/erc20/Cargo.toml
+++ b/examples/erc20/Cargo.toml
@@ -31,5 +31,3 @@ e2e = []
 [lib]
 crate-type = ["lib", "cdylib"]
 
-[lints]
-workspace = true
\ No newline at end of file
diff --git a/examples/erc721/Cargo.toml b/examples/erc721/Cargo.toml
index cb97e3d..284ecd9 100644
--- a/examples/erc721/Cargo.toml
+++ b/examples/erc721/Cargo.toml
@@ -33,5 +33,3 @@ e2e = []
 [lib]
 crate-type = ["lib", "cdylib"]
 
-[lints]
-workspace = true
\ No newline at end of file
diff --git a/examples/merkle-proofs/Cargo.toml b/examples/merkle-proofs/Cargo.toml
index d0d7eef..ff8d887 100644
--- a/examples/merkle-proofs/Cargo.toml
+++ b/examples/merkle-proofs/Cargo.toml
@@ -25,5 +25,3 @@ std = []
 [lib]
 crate-type = ["lib", "cdylib"]
 
-[lints]
-workspace = true
\ No newline at end of file
diff --git a/examples/ownable/Cargo.toml b/examples/ownable/Cargo.toml
index 166d0cd..d698503 100644
--- a/examples/ownable/Cargo.toml
+++ b/examples/ownable/Cargo.toml
@@ -7,14 +7,24 @@ publish = false
 version = "0.0.0"
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts", features = ["ownable", "erc20"] }
+openzeppelin-stylus = { path = "../../contracts", features = [
+  "ownable",
+  "erc20",
+] }
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
 mini-alloc.workspace = true
 
+[dev-dependencies]
+alloy.workspace = true
+e2e = { path = "../../lib/e2e" }
+tokio = { version = "1.12.0", features = ["full"] }
+eyre = "0.6.8"
+
 [lib]
 crate-type = ["lib", "cdylib"]
 
-[lints]
-workspace = true
\ No newline at end of file
+[features]
+default = []
+e2e = []
diff --git a/lib/motsu-proc/src/default_storage_layout.rs b/lib/motsu-proc/src/default_storage_layout.rs
index 9edfe25..e661573 100644
--- a/lib/motsu-proc/src/default_storage_layout.rs
+++ b/lib/motsu-proc/src/default_storage_layout.rs
@@ -5,7 +5,7 @@ use syn::{Data, DeriveInput};
 
 const STORAGE_WORD_BYTES: u8 = 32;
 
-pub fn impl_default_storage_layout(ast: &DeriveInput) -> TokenStream {
+pub(crate) fn impl_default_storage_layout(ast: &DeriveInput) -> TokenStream {
     let name = &ast.ident;
 
     let Data::Struct(ref data_struct) = ast.data else {
diff --git a/lib/motsu-proc/src/lib.rs b/lib/motsu-proc/src/lib.rs
index 7c0daf2..ae2f0bc 100644
--- a/lib/motsu-proc/src/lib.rs
+++ b/lib/motsu-proc/src/lib.rs
@@ -55,7 +55,7 @@ mod test;
 /// ```
 #[proc_macro_attribute]
 pub fn test(attr: TokenStream, input: TokenStream) -> TokenStream {
-    test::test(attr, input)
+    test::test(&attr, input)
 }
 
 /// Automatically implements the `Default` trait for a struct that uses
diff --git a/lib/motsu/src/shims.rs b/lib/motsu/src/shims.rs
index 3288a06..e66a942 100644
--- a/lib/motsu/src/shims.rs
+++ b/lib/motsu/src/shims.rs
@@ -2,10 +2,10 @@
 //!
 //! Most of the documentation is taken from the [Stylus source].
 //!
-//! [Stylus source]: https://github.com/OffchainLabs/stylus/blob/484efac4f56fb70f96d4890748b8ec2543d88acd/arbitrator/wasm-libraries/user-host-trait/src/lib.rs
-//!
 //! We allow unsafe here because safety is guaranteed by the Stylus team.
 //!
+//! [Stylus source]: https://github.com/OffchainLabs/stylus/blob/484efac4f56fb70f96d4890748b8ec2543d88acd/arbitrator/wasm-libraries/user-host-trait/src/lib.rs
+//!
 //! ## Motivation
 //!
 //! Without these shims we can't currently run unit tests for stylus contracts,
@@ -104,6 +104,10 @@ pub unsafe extern "C" fn native_keccak256(
 /// equivalent to that of the EVM's [`SLOAD`] opcode.
 ///
 /// [`SLOAD`]: https://www.evm.codes/#54
+///
+/// # Panics
+///
+/// May panic if unable to lock `STORAGE`.
 #[no_mangle]
 pub unsafe extern "C" fn storage_load_bytes32(key: *const u8, out: *mut u8) {
     let key = unsafe { read_bytes32(key) };
@@ -128,6 +132,10 @@ pub unsafe extern "C" fn storage_load_bytes32(key: *const u8, out: *mut u8) {
 /// persist it.
 ///
 /// [`SSTORE`]: https://www.evm.codes/#55
+///
+/// # Panics
+///
+/// May panic if unable to lock `STORAGE`.
 #[no_mangle]
 pub unsafe extern "C" fn storage_cache_bytes32(
     key: *const u8,
@@ -160,6 +168,10 @@ pub const MSG_SENDER: &[u8; 42] = b"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF";
 /// [`CALLER`]: https://www.evm.codes/#33
 /// [`DELEGATE_CALL`]: https://www.evm.codes/#f4
 /// [aliasing]: https://developer.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing
+///
+/// # Panics
+///
+/// May panic if fails to parse `MSG_SENDER` as an address.
 #[no_mangle]
 pub unsafe extern "C" fn msg_sender(sender: *mut u8) {
     let addr = const_hex::const_decode_to_array::<20>(MSG_SENDER).unwrap();
diff --git a/lib/motsu/src/storage.rs b/lib/motsu/src/storage.rs
index ab5f7a2..4dde3a4 100644
--- a/lib/motsu/src/storage.rs
+++ b/lib/motsu/src/storage.rs
@@ -6,22 +6,26 @@ use once_cell::sync::Lazy;
 use crate::shims::{Bytes32, WORD_BYTES};
 
 /// Storage mock: A global mutable key-value store.
-pub static STORAGE: Lazy<Mutex<HashMap<Bytes32, Bytes32>>> =
+pub(crate) static STORAGE: Lazy<Mutex<HashMap<Bytes32, Bytes32>>> =
     Lazy::new(|| Mutex::new(HashMap::new()));
 
 /// Read the word at address `key`.
-pub unsafe fn read_bytes32(key: *const u8) -> Bytes32 {
+pub(crate) unsafe fn read_bytes32(key: *const u8) -> Bytes32 {
     let mut res = Bytes32::default();
     ptr::copy(key, res.as_mut_ptr(), WORD_BYTES);
     res
 }
 
 /// Write the word `val` to the location pointed by `key`.
-pub unsafe fn write_bytes32(key: *mut u8, val: Bytes32) {
+pub(crate) unsafe fn write_bytes32(key: *mut u8, val: Bytes32) {
     ptr::copy(val.as_ptr(), key, WORD_BYTES);
 }
 
 /// Clears storage, removing all key-value pairs.
+///
+/// # Panics
+///
+/// May panic if the storage lock is already held by the current thread.
 pub fn reset_storage() {
     STORAGE.lock().unwrap().clear();
 }


Test patch:
diff --git a/examples/erc20/tests/erc20.rs b/examples/erc20/tests/erc20.rs
index 1fd922c..c7e73d2 100644
--- a/examples/erc20/tests/erc20.rs
+++ b/examples/erc20/tests/erc20.rs
@@ -5,6 +5,7 @@ use alloy::{
     sol,
     sol_types::{SolConstructor, SolError},
 };
+use alloy_primitives::uint;
 use e2e::{receipt, send, watch, EventExt, Panic, PanicCode, Revert, User};
 use eyre::Result;
 
@@ -16,7 +17,7 @@ sol!("src/constructor.sol");
 
 const TOKEN_NAME: &str = "Test Token";
 const TOKEN_SYMBOL: &str = "TTK";
-const CAP: usize = 1_000_000;
+const CAP: U256 = uint!(1_000_000_U256);
 
 async fn deploy(
     rpc_url: &str,
@@ -26,7 +27,7 @@ async fn deploy(
     let args = Erc20Example::constructorCall {
         name_: TOKEN_NAME.to_owned(),
         symbol_: TOKEN_SYMBOL.to_owned(),
-        cap_: cap.unwrap_or(U256::from(CAP)),
+        cap_: cap.unwrap_or(CAP),
     };
     let args = alloy::hex::encode(args.abi_encode());
     e2e::deploy(rpc_url, private_key, Some(args)).await
@@ -39,7 +40,7 @@ async fn deploy(
 #[e2e::test]
 async fn constructs(alice: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract = Erc20::new(contract_addr, &alice.signer);
+    let contract = Erc20::new(contract_addr, &alice.wallet);
 
     let Erc20::nameReturn { name } = contract.name().call().await?;
     let Erc20::symbolReturn { symbol } = contract.symbol().call().await?;
@@ -50,7 +51,7 @@ async fn constructs(alice: User) -> Result<()> {
 
     assert_eq!(name, TOKEN_NAME.to_owned());
     assert_eq!(symbol, TOKEN_SYMBOL.to_owned());
-    assert_eq!(cap, U256::from(CAP));
+    assert_eq!(cap, CAP);
     assert_eq!(decimals, 10);
     assert_eq!(total_supply, U256::ZERO);
     Ok(())
@@ -59,7 +60,7 @@ async fn constructs(alice: User) -> Result<()> {
 #[e2e::test]
 async fn mints(alice: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract = Erc20::new(contract_addr, &alice.signer);
+    let contract = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
 
     let Erc20::balanceOfReturn { balance: initial_balance } =
@@ -70,7 +71,7 @@ async fn mints(alice: User) -> Result<()> {
     assert_eq!(U256::ZERO, initial_balance);
     assert_eq!(U256::ZERO, initial_supply);
 
-    let one = U256::from(1);
+    let one = uint!(1_U256);
     let receipt = receipt!(contract.mint(alice_addr, one))?;
     receipt.emits(Erc20::Transfer {
         from: Address::ZERO,
@@ -91,7 +92,7 @@ async fn mints(alice: User) -> Result<()> {
 #[e2e::test]
 async fn mints_rejects_invalid_receiver(alice: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract = Erc20::new(contract_addr, &alice.signer);
+    let contract = Erc20::new(contract_addr, &alice.wallet);
     let invalid_receiver = Address::ZERO;
 
     let Erc20::balanceOfReturn { balance: initial_balance } =
@@ -99,7 +100,7 @@ async fn mints_rejects_invalid_receiver(alice: User) -> Result<()> {
     let Erc20::totalSupplyReturn { totalSupply: initial_supply } =
         contract.totalSupply().call().await?;
 
-    let value = U256::from(10);
+    let value = uint!(10_U256);
     let err = send!(contract.mint(invalid_receiver, value))
         .expect_err("should not mint tokens for Address::ZERO");
     assert!(err.reverted_with(Erc20::ERC20InvalidReceiver {
@@ -121,10 +122,10 @@ async fn mints_rejects_overflow(alice: User) -> Result<()> {
     let max_cap = U256::MAX;
 
     let contract_addr = deploy(alice.url(), &alice.pk(), Some(max_cap)).await?;
-    let contract = Erc20::new(contract_addr, &alice.signer);
+    let contract = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
 
-    let one = U256::from(1);
+    let one = uint!(1_U256);
 
     let _ = watch!(contract.mint(alice_addr, max_cap))?;
 
@@ -154,12 +155,12 @@ async fn mints_rejects_overflow(alice: User) -> Result<()> {
 #[e2e::test]
 async fn transfers(alice: User, bob: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
     let bob_addr = bob.address();
 
-    let balance = U256::from(10);
-    let value = U256::from(1);
+    let balance = uint!(10_U256);
+    let value = uint!(1_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -194,12 +195,12 @@ async fn transfer_rejects_insufficient_balance(
     bob: User,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
     let bob_addr = bob.address();
 
-    let balance = U256::from(10);
-    let value = U256::from(11);
+    let balance = uint!(10_U256);
+    let value = uint!(11_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -235,12 +236,12 @@ async fn transfer_rejects_insufficient_balance(
 #[e2e::test]
 async fn transfer_rejects_invalid_receiver(alice: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
     let invalid_receiver = Address::ZERO;
 
-    let balance = U256::from(10);
-    let value = U256::from(1);
+    let balance = uint!(10_U256);
+    let value = uint!(1_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -274,12 +275,12 @@ async fn transfer_rejects_invalid_receiver(alice: User) -> Result<()> {
 #[e2e::test]
 async fn approves(alice: User, bob: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract = Erc20::new(contract_addr, &alice.signer);
+    let contract = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
     let bob_addr = bob.address();
 
-    let one = U256::from(1);
-    let ten = U256::from(10);
+    let one = uint!(1_U256);
+    let ten = uint!(10_U256);
 
     let Erc20::allowanceReturn { allowance: initial_alice_bob_allowance } =
         contract.allowance(alice_addr, bob_addr).call().await?;
@@ -341,11 +342,11 @@ async fn approves(alice: User, bob: User) -> Result<()> {
 #[e2e::test]
 async fn approve_rejects_invalid_spender(alice: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract = Erc20::new(contract_addr, &alice.signer);
+    let contract = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
     let invalid_spender = Address::ZERO;
 
-    let ten = U256::from(10);
+    let ten = uint!(10_U256);
 
     let Erc20::allowanceReturn { allowance: initial_alice_spender_allowance } =
         contract.allowance(alice_addr, invalid_spender).call().await?;
@@ -391,14 +392,14 @@ async fn approve_rejects_invalid_spender(alice: User) -> Result<()> {
 #[e2e::test]
 async fn transfers_from(alice: User, bob: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
-    let contract_bob = Erc20::new(contract_addr, &bob.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
+    let contract_bob = Erc20::new(contract_addr, &bob.wallet);
 
     let alice_addr = alice.address();
     let bob_addr = bob.address();
 
-    let balance = U256::from(10);
-    let value = U256::from(1);
+    let balance = uint!(10_U256);
+    let value = uint!(1_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -442,14 +443,14 @@ async fn transfer_from_reverts_insufficient_balance(
     bob: User,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
-    let contract_bob = Erc20::new(contract_addr, &bob.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
+    let contract_bob = Erc20::new(contract_addr, &bob.wallet);
 
     let alice_addr = alice.address();
     let bob_addr = bob.address();
 
-    let balance = U256::from(1);
-    let value = U256::from(10);
+    let balance = uint!(1_U256);
+    let value = uint!(10_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -497,14 +498,14 @@ async fn transfer_from_rejects_insufficient_allowance(
     bob: User,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
-    let contract_bob = Erc20::new(contract_addr, &bob.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
+    let contract_bob = Erc20::new(contract_addr, &bob.wallet);
 
     let alice_addr = alice.address();
     let bob_addr = bob.address();
 
-    let balance = U256::from(10);
-    let value = U256::from(1);
+    let balance = uint!(10_U256);
+    let value = uint!(1_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -552,15 +553,15 @@ async fn transfer_from_rejects_invalid_receiver(
     bob: User,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
-    let contract_bob = Erc20::new(contract_addr, &bob.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
+    let contract_bob = Erc20::new(contract_addr, &bob.wallet);
 
     let alice_addr = alice.address();
     let bob_addr = bob.address();
     let invalid_receiver = Address::ZERO;
 
-    let balance = U256::from(10);
-    let value = U256::from(1);
+    let balance = uint!(10_U256);
+    let value = uint!(1_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -608,11 +609,11 @@ async fn transfer_from_rejects_invalid_receiver(
 #[e2e::test]
 async fn burns(alice: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
 
-    let balance = U256::from(10);
-    let value = U256::from(1);
+    let balance = uint!(10_U256);
+    let value = uint!(1_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -643,11 +644,11 @@ async fn burns(alice: User) -> Result<()> {
 #[e2e::test]
 async fn burn_rejects_insufficient_balance(alice: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
 
-    let balance = U256::from(10);
-    let value = U256::from(11);
+    let balance = uint!(10_U256);
+    let value = uint!(11_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -678,14 +679,14 @@ async fn burn_rejects_insufficient_balance(alice: User) -> Result<()> {
 #[e2e::test]
 async fn burns_from(alice: User, bob: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
-    let contract_bob = Erc20::new(contract_addr, &bob.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
+    let contract_bob = Erc20::new(contract_addr, &bob.wallet);
 
     let alice_addr = alice.address();
     let bob_addr = bob.address();
 
-    let balance = U256::from(10);
-    let value = U256::from(1);
+    let balance = uint!(10_U256);
+    let value = uint!(1_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -732,14 +733,14 @@ async fn burn_from_reverts_insufficient_balance(
     bob: User,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
-    let contract_bob = Erc20::new(contract_addr, &bob.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
+    let contract_bob = Erc20::new(contract_addr, &bob.wallet);
 
     let alice_addr = alice.address();
     let bob_addr = bob.address();
 
-    let balance = U256::from(1);
-    let value = U256::from(10);
+    let balance = uint!(1_U256);
+    let value = uint!(10_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -787,14 +788,14 @@ async fn burn_from_rejects_insufficient_allowance(
     bob: User,
 ) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
-    let contract_bob = Erc20::new(contract_addr, &bob.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
+    let contract_bob = Erc20::new(contract_addr, &bob.wallet);
 
     let alice_addr = alice.address();
     let bob_addr = bob.address();
 
-    let balance = U256::from(10);
-    let value = U256::from(1);
+    let balance = uint!(10_U256);
+    let value = uint!(1_U256);
 
     let _ = watch!(contract_alice.mint(alice.address(), balance))?;
 
@@ -843,12 +844,12 @@ async fn burn_from_rejects_insufficient_allowance(
 #[e2e::test]
 async fn mint_rejects_exceeding_cap(alice: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
 
-    let one = U256::from(1);
-    let two = U256::from(2);
-    let cap = U256::from(CAP);
+    let one = uint!(1_U256);
+    let two = uint!(2_U256);
+    let cap = CAP;
     let balance = cap - one;
 
     let _ = watch!(contract_alice.mint(alice_addr, balance))?;
@@ -879,11 +880,11 @@ async fn mint_rejects_exceeding_cap(alice: User) -> Result<()> {
 #[e2e::test]
 async fn mint_rejects_when_cap_reached(alice: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk(), None).await?;
-    let contract_alice = Erc20::new(contract_addr, &alice.signer);
+    let contract_alice = Erc20::new(contract_addr, &alice.wallet);
     let alice_addr = alice.address();
 
-    let one = U256::from(1);
-    let cap = U256::from(CAP);
+    let one = uint!(1_U256);
+    let cap = CAP;
     let balance = cap;
 
     let _ = watch!(contract_alice.mint(alice_addr, balance))?;
@@ -918,7 +919,7 @@ async fn should_not_deploy_capped_with_invalid_cap(alice: User) -> Result<()> {
         .await
         .expect_err("should not deploy due to `ERC20InvalidCap`");
 
-    // TODO: improve error check for contract deployment
+    // TODO: Improve error check for contract deployments.
     // Issue: https://github.com/OpenZeppelin/rust-contracts-stylus/issues/128
     // Requires strange casting
     //  ErrorResp(
diff --git a/examples/erc721/tests/erc721.rs b/examples/erc721/tests/erc721.rs
index ae99fa4..273022c 100644
--- a/examples/erc721/tests/erc721.rs
+++ b/examples/erc721/tests/erc721.rs
@@ -5,6 +5,7 @@ use alloy::{
     sol,
     sol_types::SolConstructor,
 };
+use alloy_primitives::uint;
 use e2e::{receipt, send, watch, EventExt, Revert, User};
 
 use crate::abi::Erc721;
@@ -33,7 +34,7 @@ async fn deploy(rpc_url: &str, private_key: &str) -> eyre::Result<Address> {
 #[e2e::test]
 async fn constructs(alice: User) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-    let contract = Erc721::new(contract_addr, &alice.signer);
+    let contract = Erc721::new(contract_addr, &alice.wallet);
 
     let name = contract.name().call().await?.name;
     let symbol = contract.symbol().call().await?.symbol;
@@ -46,7 +47,7 @@ async fn constructs(alice: User) -> eyre::Result<()> {
 #[e2e::test]
 async fn mints(alice: User) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-    let contract = Erc721::new(contract_addr, &alice.signer);
+    let contract = Erc721::new(contract_addr, &alice.wallet);
 
     let alice_addr = alice.address();
     let token_id = random_token_id();
@@ -55,14 +56,14 @@ async fn mints(alice: User) -> eyre::Result<()> {
     assert_eq!(owner_of, alice_addr);
 
     let balance = contract.balanceOf(alice_addr).call().await?.balance;
-    assert!(balance >= U256::from(1));
+    assert!(balance >= uint!(1_U256));
     Ok(())
 }
 
 #[e2e::test]
 async fn errors_when_reusing_token_id(alice: User) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-    let contract = Erc721::new(contract_addr, &alice.signer);
+    let contract = Erc721::new(contract_addr, &alice.wallet);
 
     let alice_addr = alice.address();
     let token_id = random_token_id();
@@ -78,7 +79,7 @@ async fn errors_when_reusing_token_id(alice: User) -> eyre::Result<()> {
 #[e2e::test]
 async fn transfers(alice: User, bob: User) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-    let contract = Erc721::new(contract_addr, &alice.signer);
+    let contract = Erc721::new(contract_addr, &alice.wallet);
 
     let alice_addr = alice.address();
     let bob_addr = bob.address();
@@ -105,7 +106,7 @@ async fn errors_when_transfer_nonexistent_token(
     bob: User,
 ) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-    let contract = Erc721::new(contract_addr, &alice.signer);
+    let contract = Erc721::new(contract_addr, &alice.wallet);
 
     let alice_addr = alice.address();
     let token_id = random_token_id();
@@ -121,7 +122,7 @@ async fn errors_when_transfer_nonexistent_token(
 #[e2e::test]
 async fn approves_token_transfer(alice: User, bob: User) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-    let contract = Erc721::new(contract_addr, &alice.signer);
+    let contract = Erc721::new(contract_addr, &alice.wallet);
 
     let alice_addr = alice.address();
     let bob_addr = bob.address();
@@ -129,7 +130,7 @@ async fn approves_token_transfer(alice: User, bob: User) -> eyre::Result<()> {
     let _ = watch!(contract.mint(alice_addr, token_id))?;
     let _ = watch!(contract.approve(bob_addr, token_id))?;
 
-    let contract = Erc721::new(contract_addr, &bob.signer);
+    let contract = Erc721::new(contract_addr, &bob.wallet);
     let _ = watch!(contract.transferFrom(alice_addr, bob_addr, token_id))?;
     let Erc721::ownerOfReturn { ownerOf } =
         contract.ownerOf(token_id).call().await?;
@@ -144,14 +145,14 @@ async fn errors_when_transfer_unapproved_token(
     bob: User,
 ) -> eyre::Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-    let contract = Erc721::new(contract_addr, &alice.signer);
+    let contract = Erc721::new(contract_addr, &alice.wallet);
 
     let alice_addr = alice.address();
     let bob_addr = bob.address();
     let token_id = random_token_id();
     let _ = watch!(contract.mint(alice_addr, token_id))?;
 
-    let contract = Erc721::new(contract_addr, &bob.signer);
+    let contract = Erc721::new(contract_addr, &bob.wallet);
     let tx = contract.transferFrom(alice_addr, bob_addr, token_id);
 
     let err = send!(tx).expect_err("should not transfer unapproved token");
diff --git a/examples/ownable/tests/abi.rs b/examples/ownable/tests/abi.rs
new file mode 100644
index 0000000..9362ae9
--- /dev/null
+++ b/examples/ownable/tests/abi.rs
@@ -0,0 +1,17 @@
+#![allow(dead_code)]
+use alloy::sol;
+
+sol!(
+    #[sol(rpc)]
+   contract Ownable {
+        error OwnableUnauthorizedAccount(address account);
+        error OwnableInvalidOwner(address owner);
+
+        #[derive(Debug, PartialEq)]
+        event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
+
+        function owner() public view virtual returns (address owner);
+        function renounceOwnership() public virtual onlyOwner;
+        function transferOwnership(address newOwner) public virtual;
+    }
+);
diff --git a/examples/ownable/tests/ownable.rs b/examples/ownable/tests/ownable.rs
new file mode 100644
index 0000000..52fc03a
--- /dev/null
+++ b/examples/ownable/tests/ownable.rs
@@ -0,0 +1,178 @@
+#![cfg(feature = "e2e")]
+
+use abi::Ownable::OwnershipTransferred;
+use alloy::{
+    primitives::Address,
+    providers::Provider,
+    rpc::types::{BlockNumberOrTag, Filter},
+    sol,
+    sol_types::{SolConstructor, SolError, SolEvent},
+};
+use e2e::{receipt, send, EventExt, Revert, User};
+use eyre::Result;
+
+use crate::abi::Ownable;
+
+mod abi;
+
+sol!("src/constructor.sol");
+
+async fn deploy(user: &User, 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
+}
+
+// ============================================================================
+// Integration Tests: Ownable
+// ============================================================================
+
+#[e2e::test]
+async fn constructs(alice: User) -> Result<()> {
+    let alice_addr = alice.address();
+    let contract_addr = deploy(&alice, alice_addr).await?;
+    let contract = Ownable::new(contract_addr, &alice.wallet);
+
+    let Ownable::ownerReturn { owner } = contract.owner().call().await?;
+    assert_eq!(owner, alice_addr);
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn emits_ownership_transfer_during_construction(
+    alice: User,
+) -> Result<()> {
+    let alice_addr = alice.address();
+
+    deploy(&alice, alice_addr).await?;
+
+    let block = alice.wallet.get_block_number().await?;
+    let filter = Filter::new()
+        .event_signature(OwnershipTransferred::SIGNATURE_HASH)
+        .from_block(BlockNumberOrTag::Number(block - 1));
+
+    let logs = alice.wallet.get_logs(&filter).await?;
+    let emitted = logs[0].log_decode::<OwnershipTransferred>()?.inner.data;
+    let expected = OwnershipTransferred {
+        previousOwner: Address::ZERO,
+        newOwner: alice_addr,
+    };
+    assert_eq!(emitted, expected);
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn rejects_zero_address_initial_owner(alice: User) -> Result<()> {
+    let err = deploy(&alice, Address::ZERO)
+        .await
+        .expect_err("should not deploy due to `OwnableInvalidOwner`");
+
+    // TODO: Improve error check for contract deployments.
+    // Issue: https://github.com/OpenZeppelin/rust-contracts-stylus/issues/128
+    let err_string = format!("{:#?}", err);
+    let expected = Ownable::OwnableInvalidOwner { owner: Address::ZERO };
+    let expected = alloy::hex::encode(expected.abi_encode());
+
+    assert!(err_string.contains(&expected));
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn transfers_ownership(alice: User, bob: User) -> Result<()> {
+    let alice_addr = alice.address();
+    let bob_addr = bob.address();
+
+    let contract_addr = deploy(&alice, alice_addr).await?;
+    let contract = Ownable::new(contract_addr, &alice.wallet);
+
+    let receipt = receipt!(contract.transferOwnership(bob_addr))?;
+    receipt.emits(OwnershipTransferred {
+        previousOwner: alice_addr,
+        newOwner: bob_addr,
+    });
+
+    let Ownable::ownerReturn { owner } = contract.owner().call().await?;
+    assert_eq!(owner, bob_addr);
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn prevents_non_owners_from_transferring(
+    alice: User,
+    bob: User,
+) -> Result<()> {
+    let alice_addr = alice.address();
+    let bob_addr = bob.address();
+
+    let contract_addr = deploy(&alice, bob_addr).await?;
+    let contract = Ownable::new(contract_addr, &alice.wallet);
+
+    let err = send!(contract.transferOwnership(bob_addr))
+        .expect_err("should not transfer when not owned");
+    err.reverted_with(Ownable::OwnableUnauthorizedAccount {
+        account: alice_addr,
+    });
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn guards_against_stuck_state(alice: User) -> Result<()> {
+    let alice_addr = alice.address();
+    let contract_addr = deploy(&alice, alice_addr).await?;
+    let contract = Ownable::new(contract_addr, &alice.wallet);
+
+    let err = send!(contract.transferOwnership(Address::ZERO))
+        .expect_err("should not transfer to Address::ZERO");
+    err.reverted_with(Ownable::OwnableInvalidOwner { owner: Address::ZERO });
+
+    let Ownable::ownerReturn { owner } = contract.owner().call().await?;
+    assert_eq!(owner, alice_addr);
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn loses_ownership_after_renouncement(alice: User) -> Result<()> {
+    let alice_addr = alice.address();
+    let contract_addr = deploy(&alice, alice_addr).await?;
+    let contract = Ownable::new(contract_addr, &alice.wallet);
+
+    let receipt = receipt!(contract.renounceOwnership())?;
+    receipt.emits(OwnershipTransferred {
+        previousOwner: alice_addr,
+        newOwner: Address::ZERO,
+    });
+
+    let Ownable::ownerReturn { owner } = contract.owner().call().await?;
+    assert_eq!(owner, Address::ZERO);
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn prevents_non_owners_from_renouncement(
+    alice: User,
+    bob: User,
+) -> Result<()> {
+    let alice_addr = alice.address();
+    let bob_addr = bob.address();
+
+    let contract_addr = deploy(&alice, alice_addr).await?;
+    let contract = Ownable::new(contract_addr, &bob.wallet);
+
+    let err = send!(contract.renounceOwnership())
+        .expect_err("should prevent non-owner from renouncing");
+    err.reverted_with(Ownable::OwnableUnauthorizedAccount {
+        account: bob_addr,
+    });
+
+    let Ownable::ownerReturn { owner } = contract.owner().call().await?;
+    assert_eq!(owner, alice_addr);
+
+    Ok(())
+}
diff --git a/lib/e2e-proc/src/lib.rs b/lib/e2e-proc/src/lib.rs
index a370a44..739bf86 100644
--- a/lib/e2e-proc/src/lib.rs
+++ b/lib/e2e-proc/src/lib.rs
@@ -1,3 +1,4 @@
+#![doc = include_str!("../README.md")]
 use proc_macro::TokenStream;
 
 mod test;
@@ -16,5 +17,5 @@ mod test;
 /// ```
 #[proc_macro_attribute]
 pub fn test(attr: TokenStream, input: TokenStream) -> TokenStream {
-    test::test(attr, input)
+    test::test(&attr, input)
 }
diff --git a/lib/e2e-proc/src/test.rs b/lib/e2e-proc/src/test.rs
index ba0043d..f469067 100644
--- a/lib/e2e-proc/src/test.rs
+++ b/lib/e2e-proc/src/test.rs
@@ -16,7 +16,7 @@ macro_rules! error {
 /// Defines an end-to-end test that injects test users through parameters.
 ///
 /// For more information see [`crate::test`].
-pub fn test(_attr: TokenStream, input: TokenStream) -> TokenStream {
+pub(crate) fn test(_attr: &TokenStream, input: TokenStream) -> TokenStream {
     let item_fn = parse_macro_input!(input as syn::ItemFn);
     let attrs = &item_fn.attrs;
     let sig = &item_fn.sig;
diff --git a/lib/e2e/README.md b/lib/e2e/README.md
index 5e6110a..241f378 100644
--- a/lib/e2e/README.md
+++ b/lib/e2e/README.md
@@ -20,25 +20,25 @@ tests suite using the `e2e` crate.
 Decorate your tests with the `test` procedural macro: a thin wrapper over
 `tokio::test` that sets up `User`s for your test.
 
-```rust
+```rust,ignore
 #[e2e::test]
 async fn users_are_funded(alice: User) -> eyre::Result<()> {
-    let balance = alice.signer.get_balance(alice.address()).await?;
+    let balance = alice.wallet.get_balance(alice.address()).await?;
     let expected = parse_ether("10")?;
     assert_eq!(expected, balance);
     Ok(())
 }
 ```
 
-A `User` is a thin wrapper over a [`LocalWallet`] and an `alloy` provider with a
-[`SignerFiller`]. Both of them are connected to the RPC endpoint defined by the
+A `User` 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
 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:
 
-```rust
+```rust,ignore
 #[e2e::test]
 async fn foo(alice: User, bob: User) -> eyre::Result<()> {
     let charlie = User::new().await?;
@@ -46,8 +46,8 @@ async fn foo(alice: User, bob: User) -> eyre::Result<()> {
 }
 ```
 
-[`LocalWallet`]: https://github.com/alloy-rs/alloy/blob/4ecb7d86882ece8a9a7a5a892b71a3c198030731/crates/signer-wallet/src/lib.rs#L37
-[`SignerFiller`]: https://github.com/alloy-rs/alloy/blob/4ecb7d86882ece8a9a7a5a892b71a3c198030731/crates/provider/src/fillers/signer.rs#L30
+[`LocalWallet`]: https://github.com/alloy-rs/alloy/blob/8aa54828c025a99bbe7e2d4fc9768605d172cc6d/crates/signer-local/src/lib.rs#L37
+[`WalletFiller`]: https://github.com/alloy-rs/alloy/blob/8aa54828c025a99bbe7e2d4fc9768605d172cc6d/crates/provider/src/fillers/wallet.rs#L30
 
 ### Contracts
 
@@ -81,7 +81,7 @@ contract Example {
 Note the `sol!` invocation with the path to the constructor -- this will
 generate the abi-encodable `Example::constructorCall` struct.
 
-```rust
+```rust,ignore
 sol!("src/constructor.sol");
 
 async fn deploy(rpc_url: &str, private_key: &str) -> eyre::Result<Address> {
@@ -96,11 +96,11 @@ async fn deploy(rpc_url: &str, private_key: &str) -> eyre::Result<Address> {
 
 You can then make calls to your contract:
 
-```rust
+```rust,ignore
 #[e2e::test]
 async fn constructs(alice: User) -> Result<()> {
     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-    let contract = Erc20::new(contract_addr, &alice.signer);
+    let contract = Erc20::new(contract_addr, &alice.wallet);
 
     let Erc20::nameReturn { name } = contract.name().call().await?;
     let Erc20::symbolReturn { symbol } = contract.symbol().call().await?;
diff --git a/lib/e2e/src/deploy.rs b/lib/e2e/src/deploy.rs
index b231643..77347db 100644
--- a/lib/e2e/src/deploy.rs
+++ b/lib/e2e/src/deploy.rs
@@ -8,6 +8,13 @@ use crate::project::Crate;
 /// Deploy and activate the contract implemented as `#[entrypoint]` in the
 /// current crate using `rpc_url`, `private_key` and the ABI-encoded constructor
 /// `args`.
+///
+/// # Errors
+///
+/// May error if:
+///
+/// - Unable to collect information about the crate required for deployment.
+/// - `koba::deploy` errors.
 pub async fn deploy(
     rpc_url: &str,
     private_key: &str,
diff --git a/lib/e2e/src/error.rs b/lib/e2e/src/error.rs
index 12bb150..6c42540 100644
--- a/lib/e2e/src/error.rs
+++ b/lib/e2e/src/error.rs
@@ -1,7 +1,10 @@
 use alloy::sol_types::SolError;
 
-// Copied from https://github.com/NomicFoundation/hardhat/blob/main/packages/hardhat-chai-matchers/src/internal/reverted/panic.ts
+/// Possible panic codes for a revert.
+///
+/// Taken from <https://github.com/NomicFoundation/hardhat/blob/main/packages/hardhat-chai-matchers/src/internal/reverted/panic.ts>
 #[derive(Debug)]
+#[allow(missing_docs)] // Pretty straightforward variant names.
 pub enum PanicCode {
     AssertionError = 0x1,
     ArithmeticOverflow = 0x11,
diff --git a/lib/e2e/src/event.rs b/lib/e2e/src/event.rs
index 0e10432..c3236cd 100644
--- a/lib/e2e/src/event.rs
+++ b/lib/e2e/src/event.rs
@@ -1,5 +1,6 @@
 use alloy::{rpc::types::eth::TransactionReceipt, sol_types::SolEvent};
 
+/// Extension trait for asserting an event gets emitted.
 pub trait EventExt<E> {
     /// Asserts the contract emitted the `expected` event.
     fn emits(&self, expected: E);
@@ -21,6 +22,6 @@ where
             .map(|log| log.inner.data)
             .any(|event| expected == event);
 
-        assert!(emitted, "Event {:?} not emitted", expected);
+        assert!(emitted, "Event {expected:?} not emitted");
     }
 }
diff --git a/lib/e2e/src/lib.rs b/lib/e2e/src/lib.rs
index c97f43e..533e7c8 100644
--- a/lib/e2e/src/lib.rs
+++ b/lib/e2e/src/lib.rs
@@ -1,3 +1,4 @@
+#![doc = include_str!("../README.md")]
 mod deploy;
 mod environment;
 mod error;
@@ -10,13 +11,13 @@ pub use deploy::deploy;
 pub use e2e_proc::test;
 pub use error::{Panic, PanicCode, Revert};
 pub use event::EventExt;
-pub use system::{provider, Provider, Signer};
+pub use system::{provider, Provider, Wallet};
 pub use user::User;
 
-/// This macro provides a shorthand for broadcasting the transaction
-/// to the network.
+/// This macro provides a shorthand for broadcasting the transaction to the
+/// network.
 ///
-/// See: https://alloy-rs.github.io/alloy/alloy_contract/struct.CallBuilder.html
+/// See: <https://alloy-rs.github.io/alloy/alloy_contract/struct.CallBuilder.html>
 ///
 /// # Examples
 ///
@@ -24,7 +25,7 @@ pub use user::User;
 /// #[e2e::test]
 /// async fn foo(alice: User) -> eyre::Result<()> {
 ///     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-///     let contract = Erc721::new(contract_addr, &alice.signer);
+///     let contract = Erc721::new(contract_addr, &alice.wallet);
 ///
 ///     let alice_addr = alice.address();
 ///     let token_id = random_token_id();
@@ -41,7 +42,7 @@ macro_rules! send {
 /// This macro provides a shorthand for broadcasting the transaction
 /// to the network, and then waiting for the given number of confirmations.
 ///
-/// See: https://alloy-rs.github.io/alloy/alloy_provider/heart/struct.PendingTransactionBuilder.html
+/// See: <https://alloy-rs.github.io/alloy/alloy_provider/heart/struct.PendingTransactionBuilder.html>
 ///
 /// # Examples
 ///
@@ -49,7 +50,7 @@ macro_rules! send {
 /// #[e2e::test]
 /// async fn foo(alice: User) -> eyre::Result<()> {
 ///     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-///     let contract = Erc721::new(contract_addr, &alice.signer);
+///     let contract = Erc721::new(contract_addr, &alice.wallet);
 ///
 ///     let alice_addr = alice.address();
 ///     let token_id = random_token_id();
@@ -67,7 +68,7 @@ macro_rules! watch {
 /// to the network, waiting for the given number of confirmations, and then
 /// fetching the transaction receipt.
 ///
-/// See: https://alloy-rs.github.io/alloy/alloy_provider/heart/struct.PendingTransactionBuilder.html
+/// See: <https://alloy-rs.github.io/alloy/alloy_provider/heart/struct.PendingTransactionBuilder.html>
 ///
 /// # Examples
 ///
@@ -75,7 +76,7 @@ macro_rules! watch {
 /// #[e2e::test]
 /// async fn foo(alice: User) -> eyre::Result<()> {
 ///     let contract_addr = deploy(alice.url(), &alice.pk()).await?;
-///     let contract = Erc721::new(contract_addr, &alice.signer);
+///     let contract = Erc721::new(contract_addr, &alice.wallet);
 ///
 ///     let alice_addr = alice.address();
 ///     let token_id = random_token_id();
diff --git a/lib/e2e/src/project.rs b/lib/e2e/src/project.rs
index 362f566..96acd1e 100644
--- a/lib/e2e/src/project.rs
+++ b/lib/e2e/src/project.rs
@@ -12,12 +12,21 @@ use toml::Table;
 /// Information about the crate subject of an integration test.
 pub(crate) struct Crate {
     /// Path to the directory where the crate's manifest lives.
-    pub manifest_dir: PathBuf,
+    pub(crate) manifest_dir: PathBuf,
     /// Path to the compiled wasm binary.
-    pub wasm: PathBuf,
+    pub(crate) wasm: PathBuf,
 }
 
 impl Crate {
+    /// Collects crate information from the environment.
+    ///
+    /// # Errors
+    ///
+    /// May error if:
+    ///
+    /// - The current working directory is invalid.
+    /// - Could not read the package name from the manifest file.
+    /// - Could not read the path to the compiled wasm binary.
     pub(crate) fn new() -> eyre::Result<Self> {
         let manifest_dir = env::current_dir()?;
         let name = read_pkg_name(&manifest_dir)?;
@@ -28,6 +37,13 @@ impl Crate {
 }
 
 /// Reads and parses the package name from a manifest in `path`.
+///
+/// # Errors
+///
+/// May error if:
+///
+/// - Unable to parse the `Cargo.toml` at `path`.
+/// - Unable to read the package name from the parsed toml file.
 fn read_pkg_name<P: AsRef<Path>>(path: P) -> eyre::Result<String> {
     let cargo_toml = path.as_ref().join("Cargo.toml");
 
@@ -47,6 +63,13 @@ fn read_pkg_name<P: AsRef<Path>>(path: P) -> eyre::Result<String> {
 /// Returns the path to the compiled wasm binary with name `name`.
 ///
 /// Note that this function works for both workspaces and standalone crates.
+///
+/// # Errors
+///
+/// May error if:
+///
+/// - Unable to read the current executable's path.
+/// - The output directory is not `target`.
 fn get_wasm(name: &str) -> eyre::Result<PathBuf> {
     let name = name.replace('-', "_");
     // Looks like
diff --git a/lib/e2e/src/system.rs b/lib/e2e/src/system.rs
index ee86264..03f58e2 100644
--- a/lib/e2e/src/system.rs
+++ b/lib/e2e/src/system.rs
@@ -11,10 +11,10 @@ use alloy::{
 };
 use eyre::Context;
 
-pub const RPC_URL_ENV_VAR_NAME: &str = "RPC_URL";
+pub(crate) const RPC_URL_ENV_VAR_NAME: &str = "RPC_URL";
 
-/// Convenience type alias that represents an Ethereum signer.
-pub type Signer = FillProvider<
+/// Convenience type alias that represents an Ethereum wallet.
+pub type Wallet = FillProvider<
     JoinFill<
         JoinFill<
             JoinFill<JoinFill<Identity, GasFiller>, NonceFiller>,
@@ -44,6 +44,11 @@ fn env(name: &str) -> eyre::Result<String> {
 }
 
 /// Returns an alloy provider connected to the `RPC_URL` rpc endpoint.
+///
+/// # Panics
+///
+/// May panic if unable to load the `RPC_URL` environment variable.
+#[must_use]
 pub fn provider() -> Provider {
     let rpc_url = env(RPC_URL_ENV_VAR_NAME)
         .expect("failed to load RPC_URL var from env")
diff --git a/lib/e2e/src/user.rs b/lib/e2e/src/user.rs
index a69d59e..5677126 100644
--- a/lib/e2e/src/user.rs
+++ b/lib/e2e/src/user.rs
@@ -10,35 +10,44 @@ use tokio::sync::{Mutex, MutexGuard};
 
 use crate::{
     environment::get_node_path,
-    system::{Signer, RPC_URL_ENV_VAR_NAME},
+    system::{Wallet, RPC_URL_ENV_VAR_NAME},
 };
 
 /// Type that corresponds to a test user.
 #[derive(Clone, Debug)]
 pub struct User {
-    pub wallet: PrivateKeySigner,
-    pub signer: Signer,
+    /// 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.
+    ///
+    /// # Errors
+    ///
+    /// May fail if funding the newly created account fails.
     pub async fn new() -> Result<Self> {
-        UserFactory::get().await.create()
+        UserFactory::create().await
     }
 
     /// Get a hex-encoded String representing this user's private key.
+    #[must_use]
     pub fn pk(&self) -> String {
-        alloy::hex::encode(self.wallet.to_bytes())
+        alloy::hex::encode(self.signer.to_bytes())
     }
 
     /// Retrieve this account's address.
+    #[must_use]
     pub fn address(&self) -> Address {
-        self.wallet.address()
+        self.signer.address()
     }
 
     /// The rpc endpoint this user's provider is connect to.
+    #[must_use]
     pub fn url(&self) -> &str {
-        self.signer.client().transport().url()
+        self.wallet.client().transport().url()
     }
 }
 
@@ -46,9 +55,8 @@ impl User {
 struct UserFactory;
 
 impl UserFactory {
-    async fn get() -> MutexGuard<'static, Self> {
-        /// Singleton User Factory.
-        ///
+    /// 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
         /// creation (otherwise the nonce will be too low).
@@ -59,9 +67,16 @@ impl UserFactory {
     }
 
     /// Create new account and fund it via nitro test node access.
-    fn create(&self) -> eyre::Result<User> {
-        let wallet = PrivateKeySigner::random();
-        let addr = wallet.address();
+    ///
+    /// # Errors
+    ///
+    /// 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;
+
+        let signer = PrivateKeySigner::random();
+        let addr = signer.address();
 
         // ./test-node.bash script send-l2 --to
         // address_0x01fA6bf4Ee48B6C95900BCcf9BEA172EF5DBd478 --ethamount 10
@@ -70,7 +85,7 @@ impl UserFactory {
             .arg("script")
             .arg("send-l2")
             .arg("--to")
-            .arg(format!("address_{}", addr))
+            .arg(format!("address_{addr}"))
             .arg("--ethamount")
             .arg("10")
             .output()?;
@@ -79,17 +94,16 @@ impl UserFactory {
             .expect("failed to load RPC_URL var from env")
             .parse()
             .expect("failed to parse RPC_URL string into a URL");
-        let signer = ProviderBuilder::new()
+        let wallet = ProviderBuilder::new()
             .with_recommended_fillers()
-            .wallet(EthereumWallet::from(wallet.clone()))
+            .wallet(EthereumWallet::from(signer.clone()))
             .on_http(rpc_url);
 
-        match output.status.success() {
-            true => Ok(User { wallet, signer }),
-            false => {
-                let err = String::from_utf8_lossy(&output.stderr);
-                bail!("user's wallet wasn't filled - address is {addr}:\n{err}")
-            }
+        if output.status.success() {
+            Ok(User { signer, wallet })
+        } else {
+            let err = String::from_utf8_lossy(&output.stderr);
+            bail!("user's wallet wasn't funded - address is {addr}:\n{err}")
         }
     }
 }
diff --git a/lib/motsu-proc/src/test.rs b/lib/motsu-proc/src/test.rs
index be218a2..fe4af7d 100644
--- a/lib/motsu-proc/src/test.rs
+++ b/lib/motsu-proc/src/test.rs
@@ -6,7 +6,7 @@ use syn::{parse_macro_input, FnArg};
 /// Defines a unit test that provides access to Stylus' execution context.
 ///
 /// For more information see [`crate::test`].
-pub fn test(_attr: TokenStream, input: TokenStream) -> TokenStream {
+pub(crate) fn test(_attr: &TokenStream, input: TokenStream) -> TokenStream {
     let item_fn = parse_macro_input!(input as syn::ItemFn);
     let attrs = &item_fn.attrs;
     let sig = &item_fn.sig;
diff --git a/lib/motsu-proc/tests/default.rs b/lib/motsu-proc/tests/default.rs
index 3fe4b10..fd38c2b 100644
--- a/lib/motsu-proc/tests/default.rs
+++ b/lib/motsu-proc/tests/default.rs
@@ -1,4 +1,4 @@
-use alloy_primitives::{address, Address, U128, U16, U256, U32, U64, U8};
+use alloy_primitives::{address, uint, Address, U128, U16, U256, U32, U64, U8};
 use stylus_sdk::stylus_proc::sol_storage;
 
 sol_storage! {
@@ -13,7 +13,7 @@ sol_storage! {
 #[motsu::test]
 fn mapping_initializes_and_updates(contract: Erc20) {
     let key = address!("a935CEC3c5Ef99D7F1016674DEFd455Ef06776C5");
-    let value = U256::from(100);
+    let value = uint!(100_U256);
     contract._balances.insert(key, value);
     let balance = contract._balances.get(key);
     assert_eq!(balance, value);
@@ -45,21 +45,21 @@ fn uint_sorted_initializes(contract: UintSorted) {
 
 #[motsu::test]
 fn uint_sorted_updates(contract: UintSorted) {
-    contract.a.set(U256::from(1));
-    contract.b.set(U128::from(2));
-    contract.c.set(U64::from(3));
-    contract.d.set(U32::from(4));
-    contract.e.set(U16::from(5));
-    contract.f.set(U8::from(6));
-    contract.g.set(U8::from(7));
+    contract.a.set(uint!(1_U256));
+    contract.b.set(uint!(2_U128));
+    contract.c.set(uint!(3_U64));
+    contract.d.set(uint!(4_U32));
+    contract.e.set(uint!(5_U16));
+    contract.f.set(uint!(6_U8));
+    contract.g.set(uint!(7_U8));
 
-    assert_eq!(contract.a.get(), U256::from(1));
-    assert_eq!(contract.b.get(), U128::from(2));
-    assert_eq!(contract.c.get(), U64::from(3));
-    assert_eq!(contract.d.get(), U32::from(4));
-    assert_eq!(contract.e.get(), U16::from(5));
-    assert_eq!(contract.f.get(), U8::from(6));
-    assert_eq!(contract.g.get(), U8::from(7));
+    assert_eq!(contract.a.get(), uint!(1_U256));
+    assert_eq!(contract.b.get(), uint!(2_U128));
+    assert_eq!(contract.c.get(), uint!(3_U64));
+    assert_eq!(contract.d.get(), uint!(4_U32));
+    assert_eq!(contract.e.get(), uint!(5_U16));
+    assert_eq!(contract.f.get(), uint!(6_U8));
+    assert_eq!(contract.g.get(), uint!(7_U8));
 }
 
 sol_storage! {
@@ -88,21 +88,21 @@ fn uint_unsorted_initializes(contract: UintUnsorted) {
 
 #[motsu::test]
 fn uint_unsorted_updates(contract: UintUnsorted) {
-    contract.a.set(U256::from(1));
-    contract.b.set(U16::from(5));
-    contract.c.set(U64::from(3));
-    contract.d.set(U8::from(6));
-    contract.e.set(U32::from(4));
-    contract.f.set(U128::from(2));
-    contract.g.set(U8::from(7));
+    contract.a.set(uint!(1_U256));
+    contract.b.set(uint!(5_U16));
+    contract.c.set(uint!(3_U64));
+    contract.d.set(uint!(6_U8));
+    contract.e.set(uint!(4_U32));
+    contract.f.set(uint!(2_U128));
+    contract.g.set(uint!(7_U8));
 
-    assert_eq!(contract.a.get(), U256::from(1));
-    assert_eq!(contract.b.get(), U16::from(5));
-    assert_eq!(contract.c.get(), U64::from(3));
-    assert_eq!(contract.d.get(), U8::from(6));
-    assert_eq!(contract.e.get(), U32::from(4));
-    assert_eq!(contract.f.get(), U128::from(2));
-    assert_eq!(contract.g.get(), U8::from(7));
+    assert_eq!(contract.a.get(), uint!(1_U256));
+    assert_eq!(contract.b.get(), uint!(5_U16));
+    assert_eq!(contract.c.get(), uint!(3_U64));
+    assert_eq!(contract.d.get(), uint!(6_U8));
+    assert_eq!(contract.e.get(), uint!(4_U32));
+    assert_eq!(contract.f.get(), uint!(2_U128));
+    assert_eq!(contract.g.get(), uint!(7_U8));
 }
 
 sol_storage! {