Repository: OpenZeppelin/rust-contracts-stylus
Pull request #141 (2024-06-20 21:34:15)

ref(docs): cleanup docs & add missing doc comments

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


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | fbaa23b93b638c23c7f3a654be80990c71f7f99f |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/contracts-stylus/deploys/6674a05add8f0d000888240d |
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/141?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin) Report
Attention: Patch coverage is `80.00000%` with `1 line` in your changes missing coverage. Please review.
> 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 [(`fbaa23b`)](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/commit/fbaa23b93b638c23c7f3a654be80990c71f7f99f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin).

> :exclamation: **Current head fbaa23b differs from pull request most recent head 24242ae**
> 
> Please [upload](https://docs.codecov.com/docs/codecov-uploader) reports for the commit 24242ae to get more accurate results.

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


| [Files](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/141?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin) | Coverage Δ | |
|---|---|---|
| [lib/e2e-proc/src/lib.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/141?src=pr&el=tree&filepath=lib%2Fe2e-proc%2Fsrc%2Flib.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-bGliL2UyZS1wcm9jL3NyYy9saWIucnM=) | `0.0% <ø> (ø)` | |
| [lib/motsu-proc/src/default\_storage\_layout.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/141?src=pr&el=tree&filepath=lib%2Fmotsu-proc%2Fsrc%2Fdefault_storage_layout.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-bGliL21vdHN1LXByb2Mvc3JjL2RlZmF1bHRfc3RvcmFnZV9sYXlvdXQucnM=) | `93.0% <100.0%> (ø)` | |
| [lib/motsu-proc/src/test.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/141?src=pr&el=tree&filepath=lib%2Fmotsu-proc%2Fsrc%2Ftest.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-bGliL21vdHN1LXByb2Mvc3JjL3Rlc3QucnM=) | `97.6% <100.0%> (ø)` | |
| [lib/motsu/src/shims.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/141?src=pr&el=tree&filepath=lib%2Fmotsu%2Fsrc%2Fshims.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-bGliL21vdHN1L3NyYy9zaGltcy5ycw==) | `92.8% <ø> (ø)` | |
| [lib/motsu/src/storage.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/141?src=pr&el=tree&filepath=lib%2Fmotsu%2Fsrc%2Fstorage.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-bGliL21vdHN1L3NyYy9zdG9yYWdlLnJz) | `100.0% <100.0%> (ø)` | |
| [lib/e2e-proc/src/test.rs](https://app.codecov.io/gh/OpenZeppelin/rust-contracts-stylus/pull/141?src=pr&el=tree&filepath=lib%2Fe2e-proc%2Fsrc%2Ftest.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenZeppelin#diff-bGliL2UyZS1wcm9jL3NyYy90ZXN0LnJz) | `0.0% <0.0%> (ø)` | |

</details>

Patch:
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/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..16c86df 100644
--- a/examples/ownable/Cargo.toml
+++ b/examples/ownable/Cargo.toml
@@ -7,7 +7,10 @@ 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
@@ -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/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..6875801 100644
--- a/examples/erc20/tests/erc20.rs
+++ b/examples/erc20/tests/erc20.rs
@@ -39,7 +39,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?;
@@ -59,7 +59,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 } =
@@ -91,7 +91,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 } =
@@ -121,7 +121,7 @@ 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);
@@ -154,7 +154,7 @@ 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();
 
@@ -194,7 +194,7 @@ 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();
 
@@ -235,7 +235,7 @@ 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;
 
@@ -274,7 +274,7 @@ 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();
 
@@ -341,7 +341,7 @@ 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;
 
@@ -391,8 +391,8 @@ 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();
@@ -442,8 +442,8 @@ 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();
@@ -497,8 +497,8 @@ 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();
@@ -552,8 +552,8 @@ 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();
@@ -608,7 +608,7 @@ 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);
@@ -643,7 +643,7 @@ 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);
@@ -678,8 +678,8 @@ 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();
@@ -732,8 +732,8 @@ 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();
@@ -787,8 +787,8 @@ 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();
@@ -843,7 +843,7 @@ 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);
@@ -879,7 +879,7 @@ 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);
diff --git a/examples/erc721/tests/erc721.rs b/examples/erc721/tests/erc721.rs
index ae99fa4..b43bd0d 100644
--- a/examples/erc721/tests/erc721.rs
+++ b/examples/erc721/tests/erc721.rs
@@ -33,7 +33,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 +46,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();
@@ -62,7 +62,7 @@ async fn mints(alice: User) -> eyre::Result<()> {
 #[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 +78,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 +105,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 +121,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 +129,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 +144,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/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;