Repository: OpenZeppelin/rust-contracts-stylus
Pull request #254 (2024-08-22 08:27:50)

feat: cache contracts before benching
Use the `cargo stylus cache` to cache contracts in the `CacheManager` before running benches.


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


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | ad38c92eb184ee0fb793f5688b46e06d1ed0a500 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/contracts-stylus/deploys/66c6f6892711780008371913 |
alexfertel: > Looks good

Doesn't work 😢 

Patch:
diff --git a/.github/workflows/check-publish.yml b/.github/workflows/check-publish.yml
new file mode 100644
index 0000000..67bbb31
--- /dev/null
+++ b/.github/workflows/check-publish.yml
@@ -0,0 +1,42 @@
+name: check-publish
+# This workflow checks that the libraries can be published on crates.io.
+permissions:
+  contents: read
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+env:
+  CARGO_TERM_COLOR: always
+jobs:
+  check-publish:
+    name: Check publish on crates.io
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: set up rust
+        uses: dtolnay/rust-toolchain@master
+        id: toolchain
+        with:
+          target: wasm32-unknown-unknown
+          components: rust-src
+          toolchain: nightly-2024-01-01
+
+      - uses: Swatinem/rust-cache@v2
+
+      - name: check motsu-proc
+        run: cargo publish -p motsu-proc --dry-run
+
+      - name: check motsu
+        run: cargo publish -p motsu --dry-run
+
+      - name: check openzeppelin-crypto
+        run: cargo publish -p openzeppelin-crypto --target wasm32-unknown-unknown  --dry-run
+
+        # TODO: https://github.com/OpenZeppelin/rust-contracts-stylus/issues/291
+      # - name: check openzeppelin-stylus
+        # run: cargo publish -p openzeppelin-stylus --target wasm32-unknown-unknown  --dry-run
diff --git a/.github/workflows/gas-bench.yml b/.github/workflows/gas-bench.yml
index 67a6f12..a14bf25 100644
--- a/.github/workflows/gas-bench.yml
+++ b/.github/workflows/gas-bench.yml
@@ -31,6 +31,9 @@ jobs:
         with:
           key: "gas-bench"
 
+      - name: install cargo-stylus
+        run: cargo install cargo-stylus@0.5.1
+
       - name: install solc
         run: |
           curl -LO https://github.com/ethereum/solidity/releases/download/v0.8.24/solc-static-linux
diff --git a/Cargo.lock b/Cargo.lock
index ebb25bb..a756c69 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,7 +4,7 @@ version = 3
 
 [[package]]
 name = "access-control-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -794,7 +794,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
 [[package]]
 name = "basic-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy-primitives",
  "mini-alloc",
@@ -816,12 +816,13 @@ dependencies = [
 
 [[package]]
 name = "benches"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
  "e2e",
  "eyre",
+ "futures",
  "keccak-const",
  "koba",
  "openzeppelin-stylus",
@@ -1262,7 +1263,7 @@ dependencies = [
 
 [[package]]
 name = "cryptography-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1514,7 +1515,7 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "erc20-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1529,7 +1530,7 @@ dependencies = [
 
 [[package]]
 name = "erc20-permit-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1544,7 +1545,7 @@ dependencies = [
 
 [[package]]
 name = "erc721-consecutive-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1561,7 +1562,7 @@ dependencies = [
 
 [[package]]
 name = "erc721-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1577,7 +1578,7 @@ dependencies = [
 
 [[package]]
 name = "erc721-metadata-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -2360,7 +2361,7 @@ checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
 
 [[package]]
 name = "merkle-proofs-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy-primitives",
  "alloy-sol-types",
diff --git a/Cargo.toml b/Cargo.toml
index f196fd1..ed00cd5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,8 +46,8 @@ resolver = "2"
 authors = ["OpenZeppelin"]
 edition = "2021"
 license = "MIT"
-keywords = ["arbitrum", "ethereum", "stylus"]
 repository = "https://github.com/OpenZeppelin/rust-contracts-stylus"
+version = "0.1.0-rc"
 
 [workspace.lints.rust]
 missing_docs = "warn"
@@ -89,12 +89,21 @@ rand = "0.8.5"
 regex = "1.10.4"
 tiny-keccak = { version = "2.0.2", features = ["keccak"] }
 tokio = { version = "1.12.0", features = ["full"] }
+futures = "0.3.30"
 
 # procedural macros
 syn = { version = "2.0.58", features = ["full"] }
 proc-macro2 = "1.0.79"
 quote = "1.0.35"
 
+# members
+openzeppelin-stylus = { path = "contracts" }
+openzeppelin-crypto = { path = "lib/crypto" }
+motsu = { path = "lib/motsu"}
+motsu-proc = { path = "lib/motsu-proc", version = "0.1.0" }
+e2e = { path = "lib/e2e" }
+e2e-proc = {path = "lib/e2e-proc"}
+
 [profile.release]
 codegen-units = 1
 panic = "abort"
diff --git a/benches/Cargo.toml b/benches/Cargo.toml
index 921de2f..5cb17d8 100644
--- a/benches/Cargo.toml
+++ b/benches/Cargo.toml
@@ -4,15 +4,16 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives = { workspace = true, features = ["tiny-keccak"] }
 alloy.workspace = true
 tokio.workspace = true
+futures.workspace = true
 eyre.workspace = true
 koba.workspace = true
-e2e = { path = "../lib/e2e" }
+e2e.workspace = true
 serde = "1.0.203"
 keccak-const = "0.2.0"
diff --git a/benches/src/access_control.rs b/benches/src/access_control.rs
index 7c9b797..12ccfaa 100644
--- a/benches/src/access_control.rs
+++ b/benches/src/access_control.rs
@@ -8,7 +8,10 @@ use alloy::{
 };
 use e2e::{receipt, Account};
 
-use crate::report::Report;
+use crate::{
+    report::{ContractReport, FunctionReport},
+    CacheOpt,
+};
 
 sol!(
     #[sol(rpc)]
@@ -33,7 +36,22 @@ const ROLE: [u8; 32] =
 const NEW_ADMIN_ROLE: [u8; 32] =
     hex!("879ce0d4bfd332649ca3552efe772a38d64a315eb70ab69689fd309c735946b5");
 
-pub async fn bench() -> eyre::Result<Report> {
+pub async fn bench() -> eyre::Result<ContractReport> {
+    let receipts = run_with(CacheOpt::None).await?;
+    let report = receipts
+        .into_iter()
+        .try_fold(ContractReport::new("AccessControl"), ContractReport::add)?;
+
+    let cached_receipts = run_with(CacheOpt::Bid(0)).await?;
+    let report = cached_receipts
+        .into_iter()
+        .try_fold(report, ContractReport::add_cached)?;
+
+    Ok(report)
+}
+pub async fn run_with(
+    cache_opt: CacheOpt,
+) -> eyre::Result<Vec<FunctionReport>> {
     let alice = Account::new().await?;
     let alice_addr = alice.address();
     let alice_wallet = ProviderBuilder::new()
@@ -50,7 +68,8 @@ pub async fn bench() -> eyre::Result<Report> {
         .wallet(EthereumWallet::from(bob.signer.clone()))
         .on_http(bob.url().parse()?);
 
-    let contract_addr = deploy(&alice).await;
+    let contract_addr = deploy(&alice, cache_opt).await?;
+
     let contract = AccessControl::new(contract_addr, &alice_wallet);
     let contract_bob = AccessControl::new(contract_addr, &bob_wallet);
 
@@ -66,14 +85,17 @@ pub async fn bench() -> eyre::Result<Report> {
         (setRoleAdminCall::SIGNATURE, receipt!(contract.setRoleAdmin(ROLE.into(), NEW_ADMIN_ROLE.into()))?),
     ];
 
-    let report = receipts
+    receipts
         .into_iter()
-        .try_fold(Report::new("AccessControl"), Report::add)?;
-    Ok(report)
+        .map(FunctionReport::new)
+        .collect::<eyre::Result<Vec<_>>>()
 }
 
-async fn deploy(account: &Account) -> Address {
+async fn deploy(
+    account: &Account,
+    cache_opt: CacheOpt,
+) -> eyre::Result<Address> {
     let args = AccessControl::constructorCall {};
     let args = alloy::hex::encode(args.abi_encode());
-    crate::deploy(account, "access-control", Some(args)).await
+    crate::deploy(account, "access-control", Some(args), cache_opt).await
 }
diff --git a/benches/src/erc20.rs b/benches/src/erc20.rs
index 22f2aff..bcf6343 100644
--- a/benches/src/erc20.rs
+++ b/benches/src/erc20.rs
@@ -9,7 +9,10 @@ use alloy::{
 use alloy_primitives::U256;
 use e2e::{receipt, Account};
 
-use crate::report::Report;
+use crate::{
+    report::{ContractReport, FunctionReport},
+    CacheOpt,
+};
 
 sol!(
     #[sol(rpc)]
@@ -39,7 +42,23 @@ const TOKEN_NAME: &str = "Test Token";
 const TOKEN_SYMBOL: &str = "TTK";
 const CAP: U256 = uint!(1_000_000_U256);
 
-pub async fn bench() -> eyre::Result<Report> {
+pub async fn bench() -> eyre::Result<ContractReport> {
+    let reports = run_with(CacheOpt::None).await?;
+    let report = reports
+        .into_iter()
+        .try_fold(ContractReport::new("Erc20"), ContractReport::add)?;
+
+    let cached_reports = run_with(CacheOpt::Bid(0)).await?;
+    let report = cached_reports
+        .into_iter()
+        .try_fold(report, ContractReport::add_cached)?;
+
+    Ok(report)
+}
+
+pub async fn run_with(
+    cache_opt: CacheOpt,
+) -> eyre::Result<Vec<FunctionReport>> {
     let alice = Account::new().await?;
     let alice_addr = alice.address();
     let alice_wallet = ProviderBuilder::new()
@@ -56,7 +75,8 @@ pub async fn bench() -> eyre::Result<Report> {
         .wallet(EthereumWallet::from(bob.signer.clone()))
         .on_http(bob.url().parse()?);
 
-    let contract_addr = deploy(&alice).await;
+    let contract_addr = deploy(&alice, cache_opt).await?;
+
     let contract = Erc20::new(contract_addr, &alice_wallet);
     let contract_bob = Erc20::new(contract_addr, &bob_wallet);
 
@@ -79,17 +99,21 @@ pub async fn bench() -> eyre::Result<Report> {
         (transferFromCall::SIGNATURE, receipt!(contract_bob.transferFrom(alice_addr, bob_addr, uint!(4_U256)))?),
     ];
 
-    let report =
-        receipts.into_iter().try_fold(Report::new("Erc20"), Report::add)?;
-    Ok(report)
+    receipts
+        .into_iter()
+        .map(FunctionReport::new)
+        .collect::<eyre::Result<Vec<_>>>()
 }
 
-async fn deploy(account: &Account) -> Address {
+async fn deploy(
+    account: &Account,
+    cache_opt: CacheOpt,
+) -> eyre::Result<Address> {
     let args = Erc20Example::constructorCall {
         name_: TOKEN_NAME.to_owned(),
         symbol_: TOKEN_SYMBOL.to_owned(),
         cap_: CAP,
     };
     let args = alloy::hex::encode(args.abi_encode());
-    crate::deploy(account, "erc20", Some(args)).await
+    crate::deploy(account, "erc20", Some(args), cache_opt).await
 }
diff --git a/benches/src/erc721.rs b/benches/src/erc721.rs
index b5dc4bb..b00729a 100644
--- a/benches/src/erc721.rs
+++ b/benches/src/erc721.rs
@@ -8,7 +8,10 @@ use alloy::{
 };
 use e2e::{receipt, Account};
 
-use crate::report::Report;
+use crate::{
+    report::{ContractReport, FunctionReport},
+    CacheOpt,
+};
 
 sol!(
     #[sol(rpc)]
@@ -29,7 +32,23 @@ sol!(
 
 sol!("../examples/erc721/src/constructor.sol");
 
-pub async fn bench() -> eyre::Result<Report> {
+pub async fn bench() -> eyre::Result<ContractReport> {
+    let reports = run_with(CacheOpt::None).await?;
+    let report = reports
+        .into_iter()
+        .try_fold(ContractReport::new("Erc721"), ContractReport::add)?;
+
+    let cached_reports = run_with(CacheOpt::Bid(0)).await?;
+    let report = cached_reports
+        .into_iter()
+        .try_fold(report, ContractReport::add_cached)?;
+
+    Ok(report)
+}
+
+pub async fn run_with(
+    cache_opt: CacheOpt,
+) -> eyre::Result<Vec<FunctionReport>> {
     let alice = Account::new().await?;
     let alice_addr = alice.address();
     let alice_wallet = ProviderBuilder::new()
@@ -41,7 +60,8 @@ pub async fn bench() -> eyre::Result<Report> {
     let bob = Account::new().await?;
     let bob_addr = bob.address();
 
-    let contract_addr = deploy(&alice).await;
+    let contract_addr = deploy(&alice, cache_opt).await?;
+
     let contract = Erc721::new(contract_addr, &alice_wallet);
 
     let token_1 = uint!(1_U256);
@@ -70,13 +90,17 @@ pub async fn bench() -> eyre::Result<Report> {
         (burnCall::SIGNATURE, receipt!(contract.burn(token_1))?),
     ];
 
-    let report =
-        receipts.into_iter().try_fold(Report::new("Erc721"), Report::add)?;
-    Ok(report)
+    receipts
+        .into_iter()
+        .map(FunctionReport::new)
+        .collect::<eyre::Result<Vec<_>>>()
 }
 
-async fn deploy(account: &Account) -> Address {
+async fn deploy(
+    account: &Account,
+    cache_opt: CacheOpt,
+) -> eyre::Result<Address> {
     let args = Erc721Example::constructorCall {};
     let args = alloy::hex::encode(args.abi_encode());
-    crate::deploy(account, "erc721", Some(args)).await
+    crate::deploy(account, "erc721", Some(args), cache_opt).await
 }
diff --git a/benches/src/lib.rs b/benches/src/lib.rs
index 85a37ca..c884bcd 100644
--- a/benches/src/lib.rs
+++ b/benches/src/lib.rs
@@ -1,3 +1,5 @@
+use std::process::Command;
+
 use alloy::{
     primitives::Address,
     rpc::types::{
@@ -7,6 +9,7 @@ use alloy::{
 };
 use alloy_primitives::U128;
 use e2e::{Account, ReceiptExt};
+use eyre::WrapErr;
 use koba::config::{Deploy, Generate, PrivateKey};
 use serde::Deserialize;
 
@@ -16,8 +19,6 @@ pub mod erc721;
 pub mod merkle_proofs;
 pub mod report;
 
-const RPC_URL: &str = "http://localhost:8547";
-
 #[derive(Debug, Deserialize)]
 struct ArbOtherFields {
     #[serde(rename = "gasUsedForL1")]
@@ -27,23 +28,24 @@ struct ArbOtherFields {
     l1_block_number: String,
 }
 
+/// Cache options for the contract.
+/// `Bid(0)` will likely cache the contract on the nitro test node.
+pub enum CacheOpt {
+    None,
+    Bid(u32),
+}
+
 type ArbTxReceipt =
     WithOtherFields<TransactionReceipt<AnyReceiptEnvelope<Log>>>;
 
-fn get_l2_gas_used(receipt: &ArbTxReceipt) -> eyre::Result<u128> {
-    let l2_gas = receipt.gas_used;
-    let arb_fields: ArbOtherFields = receipt.other.deserialize_as()?;
-    let l1_gas = arb_fields.gas_used_for_l1.to::<u128>();
-    Ok(l2_gas - l1_gas)
-}
-
 async fn deploy(
     account: &Account,
     contract_name: &str,
     args: Option<String>,
-) -> Address {
+    cache_opt: CacheOpt,
+) -> eyre::Result<Address> {
     let manifest_dir =
-        std::env::current_dir().expect("should get current dir from env");
+        std::env::current_dir().context("should get current dir from env")?;
 
     let wasm_path = manifest_dir
         .join("target")
@@ -53,7 +55,7 @@ async fn deploy(
     let sol_path = args.as_ref().map(|_| {
         manifest_dir
             .join("examples")
-            .join(format!("{}", contract_name))
+            .join(contract_name)
             .join("src")
             .join("constructor.sol")
     });
@@ -72,14 +74,46 @@ async fn deploy(
             keystore_path: None,
             keystore_password_path: None,
         },
-        endpoint: RPC_URL.to_owned(),
+        endpoint: env("RPC_URL")?,
         deploy_only: false,
         quiet: true,
     };
 
-    koba::deploy(&config)
+    let address = koba::deploy(&config)
         .await
         .expect("should deploy contract")
-        .address()
-        .expect("should return contract address")
+        .address()?;
+
+    if let CacheOpt::Bid(bid) = cache_opt {
+        cache_contract(account, address, bid)?;
+    }
+
+    Ok(address)
+}
+
+/// Try to cache a contract on the stylus network.
+/// Already cached contracts won't be cached, and this function will not return
+/// an error.
+/// Output will be forwarded to the child process.
+fn cache_contract(
+    account: &Account,
+    contract_addr: Address,
+    bid: u32,
+) -> eyre::Result<()> {
+    // We don't need a status code.
+    // Since it is not zero when the contract is already cached.
+    let _ = Command::new("cargo")
+        .args(["stylus", "cache", "bid"])
+        .args(["-e", &env("RPC_URL")?])
+        .args(["--private-key", &format!("0x{}", account.pk())])
+        .arg(contract_addr.to_string())
+        .arg(bid.to_string())
+        .status()
+        .context("failed to execute `cargo stylus cache bid` command")?;
+    Ok(())
+}
+
+/// Load the `name` environment variable.
+fn env(name: &str) -> eyre::Result<String> {
+    std::env::var(name).wrap_err(format!("failed to load {name}"))
 }
diff --git a/benches/src/main.rs b/benches/src/main.rs
index cd4e0e3..52278d7 100644
--- a/benches/src/main.rs
+++ b/benches/src/main.rs
@@ -1,18 +1,21 @@
-use benches::{access_control, erc20, erc721, merkle_proofs, report::Reports};
+use benches::{
+    access_control, erc20, erc721, merkle_proofs, report::BenchmarkReport,
+};
+use futures::FutureExt;
 
 #[tokio::main]
 async fn main() -> eyre::Result<()> {
-    let reports = tokio::join!(
-        access_control::bench(),
-        erc20::bench(),
-        erc721::bench(),
-        merkle_proofs::bench()
-    );
-
-    let reports = [reports.0?, reports.1?, reports.2?, reports.3?];
-    let report =
-        reports.into_iter().fold(Reports::default(), Reports::merge_with);
+    let report = futures::future::try_join_all([
+        access_control::bench().boxed(),
+        erc20::bench().boxed(),
+        erc721::bench().boxed(),
+        merkle_proofs::bench().boxed(),
+    ])
+    .await?
+    .into_iter()
+    .fold(BenchmarkReport::default(), BenchmarkReport::merge_with);
 
+    println!();
     println!("{report}");
 
     Ok(())
diff --git a/benches/src/merkle_proofs.rs b/benches/src/merkle_proofs.rs
index 92a7d55..fd98652 100644
--- a/benches/src/merkle_proofs.rs
+++ b/benches/src/merkle_proofs.rs
@@ -8,7 +8,10 @@ use alloy::{
 };
 use e2e::{receipt, Account};
 
-use crate::report::Report;
+use crate::{
+    report::{ContractReport, FunctionReport},
+    CacheOpt,
+};
 
 sol!(
     #[sol(rpc)]
@@ -57,7 +60,23 @@ const PROOF: [[u8; 32]; 16] = bytes_array! {
     "fd47b6c292f51911e8dfdc3e4f8bd127773b17f25b7a554beaa8741e99c41208",
 };
 
-pub async fn bench() -> eyre::Result<Report> {
+pub async fn bench() -> eyre::Result<ContractReport> {
+    let reports = run_with(CacheOpt::None).await?;
+    let report = reports
+        .into_iter()
+        .try_fold(ContractReport::new("MerkleProofs"), ContractReport::add)?;
+
+    let cached_reports = run_with(CacheOpt::Bid(0)).await?;
+    let report = cached_reports
+        .into_iter()
+        .try_fold(report, ContractReport::add_cached)?;
+
+    Ok(report)
+}
+
+pub async fn run_with(
+    cache_opt: CacheOpt,
+) -> eyre::Result<Vec<FunctionReport>> {
     let alice = Account::new().await?;
     let alice_wallet = ProviderBuilder::new()
         .network::<AnyNetwork>()
@@ -65,7 +84,8 @@ pub async fn bench() -> eyre::Result<Report> {
         .wallet(EthereumWallet::from(alice.signer.clone()))
         .on_http(alice.url().parse()?);
 
-    let contract_addr = deploy(&alice).await;
+    let contract_addr = deploy(&alice, cache_opt).await?;
+
     let contract = Verifier::new(contract_addr, &alice_wallet);
 
     let proof = PROOF.map(|h| h.into()).to_vec();
@@ -75,12 +95,15 @@ pub async fn bench() -> eyre::Result<Report> {
         receipt!(contract.verify(proof, ROOT.into(), LEAF.into()))?,
     )];
 
-    let report = receipts
+    receipts
         .into_iter()
-        .try_fold(Report::new("MerkleProofs"), Report::add)?;
-    Ok(report)
+        .map(FunctionReport::new)
+        .collect::<eyre::Result<Vec<_>>>()
 }
 
-async fn deploy(account: &Account) -> Address {
-    crate::deploy(account, "merkle-proofs", None).await
+async fn deploy(
+    account: &Account,
+    cache_opt: CacheOpt,
+) -> eyre::Result<Address> {
+    crate::deploy(account, "merkle-proofs", None, cache_opt).await
 }
diff --git a/benches/src/report.rs b/benches/src/report.rs
index 3a1184b..49b5fe7 100644
--- a/benches/src/report.rs
+++ b/benches/src/report.rs
@@ -1,64 +1,157 @@
-use std::fmt::Display;
+use std::{collections::HashMap, fmt::Display};
 
-use crate::{get_l2_gas_used, ArbTxReceipt};
+use crate::{ArbOtherFields, ArbTxReceipt};
 
 const SEPARATOR: &str = "::";
 
 #[derive(Debug)]
-pub struct Report {
+pub struct FunctionReport {
+    sig: String,
+    gas: u128,
+}
+
+impl FunctionReport {
+    pub(crate) fn new(receipt: (&str, ArbTxReceipt)) -> eyre::Result<Self> {
+        Ok(FunctionReport {
+            sig: receipt.0.to_owned(),
+            gas: get_l2_gas_used(&receipt.1)?,
+        })
+    }
+}
+
+#[derive(Debug)]
+pub struct ContractReport {
     contract: String,
-    fns: Vec<(String, u128)>,
+    functions: Vec<FunctionReport>,
+    functions_cached: Vec<FunctionReport>,
 }
 
-impl Report {
+impl ContractReport {
     pub fn new(contract: &str) -> Self {
-        Report { contract: contract.to_owned(), fns: vec![] }
+        ContractReport {
+            contract: contract.to_owned(),
+            functions: vec![],
+            functions_cached: vec![],
+        }
+    }
+
+    pub fn add(mut self, fn_report: FunctionReport) -> eyre::Result<Self> {
+        self.functions.push(fn_report);
+        Ok(self)
     }
 
-    pub fn add(mut self, receipt: (&str, ArbTxReceipt)) -> eyre::Result<Self> {
-        let gas = get_l2_gas_used(&receipt.1)?;
-        self.fns.push((receipt.0.to_owned(), gas));
+    pub fn add_cached(
+        mut self,
+        fn_report: FunctionReport,
+    ) -> eyre::Result<Self> {
+        self.functions_cached.push(fn_report);
         Ok(self)
     }
 
-    fn get_longest_signature(&self) -> usize {
+    fn signature_max_len(&self) -> usize {
         let prefix_len = self.contract.len() + SEPARATOR.len();
-        self.fns
+        self.functions
+            .iter()
+            .map(|FunctionReport { sig: name, .. }| prefix_len + name.len())
+            .max()
+            .unwrap_or_default()
+    }
+
+    fn gas_max_len(&self) -> usize {
+        self.functions
+            .iter()
+            .map(|FunctionReport { gas, .. }| gas.to_string().len())
+            .max()
+            .unwrap_or_default()
+    }
+
+    fn gas_cached_max_len(&self) -> usize {
+        self.functions_cached
             .iter()
-            .map(|(sig, _)| prefix_len + sig.len())
+            .map(|FunctionReport { gas, .. }| gas.to_string().len())
             .max()
             .unwrap_or_default()
     }
 }
 
 #[derive(Debug, Default)]
-pub struct Reports(Vec<Report>);
+pub struct BenchmarkReport(Vec<ContractReport>);
 
-impl Reports {
-    pub fn merge_with(mut self, report: Report) -> Self {
+impl BenchmarkReport {
+    pub fn merge_with(mut self, report: ContractReport) -> Self {
         self.0.push(report);
         self
     }
-}
 
-impl Display for Reports {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let reports = &self.0;
-        let width = reports
+    pub fn column_width(
+        &self,
+        column_value: impl FnMut(&ContractReport) -> usize,
+        header: &str,
+    ) -> usize {
+        self.0
             .iter()
-            .map(Report::get_longest_signature)
+            .map(column_value)
+            .chain(std::iter::once(header.len()))
             .max()
-            .unwrap_or_default();
+            .unwrap_or_default()
+    }
+}
+
+impl Display for BenchmarkReport {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        const HEADER_SIG: &str = "Contract::function";
+        const HEADER_GAS_CACHED: &str = "Cached";
+        const HEADER_GAS: &str = "Not Cached";
+
+        // Calculating the width of table columns.
+        let width1 =
+            self.column_width(ContractReport::signature_max_len, HEADER_SIG);
+        let width2 = self.column_width(
+            ContractReport::gas_cached_max_len,
+            HEADER_GAS_CACHED,
+        );
+        let width3 = self.column_width(ContractReport::gas_max_len, HEADER_GAS);
 
-        for report in reports {
+        // Print headers for the table columns.
+        writeln!(
+            f,
+            "| {HEADER_SIG:<width1$} | {HEADER_GAS_CACHED:>width2$} | {HEADER_GAS:>width3$} |"
+        )?;
+        writeln!(
+            f,
+            "| {:->width1$} | {:->width2$} | {:->width3$} |",
+            "", "", ""
+        )?;
+
+        // Merging a non-cached gas report with a cached one.
+        for report in &self.0 {
             let prefix = format!("{}{SEPARATOR}", report.contract);
+            let gas: HashMap<_, _> = report
+                .functions
+                .iter()
+                .map(|func| (&*func.sig, func.gas))
+                .collect();
+
+            for report_cached in &report.functions_cached {
+                let sig = &*report_cached.sig;
+                let gas_cached = &report_cached.gas;
+                let gas = gas[sig];
 
-            for (sig, gas) in &report.fns {
-                let signature = format!("{prefix}{sig}");
-                writeln!(f, "{signature:<width$} {gas:>10}")?;
+                let full_sig = format!("{prefix}{sig}");
+                writeln!(
+                    f,
+                    "| {full_sig:<width1$} | {gas_cached:>width2$} | {gas:>width3$} |"
+                )?;
             }
         }
 
         Ok(())
     }
 }
+
+fn get_l2_gas_used(receipt: &ArbTxReceipt) -> eyre::Result<u128> {
+    let l2_gas = receipt.gas_used;
+    let arb_fields: ArbOtherFields = receipt.other.deserialize_as()?;
+    let l1_gas = arb_fields.gas_used_for_l1.to::<u128>();
+    Ok(l2_gas - l1_gas)
+}
diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml
index d7c6434..b6f9d77 100644
--- a/contracts/Cargo.toml
+++ b/contracts/Cargo.toml
@@ -1,12 +1,12 @@
 [package]
 name = "openzeppelin-stylus"
-categories = ["no-std", "wasm"]
 description = "OpenZeppelin Contracts for Stylus"
 edition.workspace = true
-keywords.workspace = true
+categories = ["cryptography::cryptocurrencies", "no-std", "wasm"]
+keywords = ["arbitrum", "ethereum", "stylus", "smart-contracts", "standards"]
 license.workspace = true
 repository.workspace = true
-version = "0.1.0-rc"
+version.workspace = true
 
 [dependencies]
 alloy-primitives.workspace = true
@@ -18,7 +18,7 @@ keccak-const.workspace = true
 
 [dev-dependencies]
 alloy-primitives = { workspace = true, features = ["arbitrary"] }
-motsu = { path = "../lib/motsu" }
+motsu.workspace = true
 rand.workspace = true
 
 [features]
diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs
index 35a8844..d5f173a 100644
--- a/contracts/src/token/erc721/extensions/consecutive.rs
+++ b/contracts/src/token/erc721/extensions/consecutive.rs
@@ -801,13 +801,16 @@ mod tests {
                     ERC721ExceededMaxBatchMint, Erc721Consecutive, Error,
                 },
                 tests::random_token_id,
-                ERC721InvalidReceiver, ERC721NonexistentToken, IErc721,
+                ERC721IncorrectOwner, ERC721InvalidApprover,
+                ERC721InvalidReceiver, ERC721InvalidSender,
+                ERC721NonexistentToken, IErc721,
             },
         },
         utils::structs::checkpoints::U96,
     };
 
     const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526");
+    const DAVE: Address = address!("0BB78F7e7132d1651B4Fd884B7624394e92156F1");
 
     fn init(
         contract: &mut Erc721Consecutive,
@@ -858,6 +861,43 @@ mod tests {
         assert_eq!(balance2, balance1 + uint!(1_U256));
     }
 
+    #[motsu::test]
+    fn error_when_minting_token_id_twice(contract: Erc721Consecutive) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+        contract
+            ._mint(alice, token_id)
+            .expect("should mint the token a first time");
+        let err = contract
+            ._mint(alice, token_id)
+            .expect_err("should not mint a token with `token_id` twice");
+
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::InvalidSender(ERC721InvalidSender {
+                sender: Address::ZERO
+            }))
+        ));
+    }
+
+    #[motsu::test]
+    fn error_when_minting_token_invalid_receiver(contract: Erc721Consecutive) {
+        let invalid_receiver = Address::ZERO;
+
+        let token_id = random_token_id();
+
+        let err = contract
+            ._mint(invalid_receiver, token_id)
+            .expect_err("should not mint a token for invalid receiver");
+
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::InvalidReceiver(ERC721InvalidReceiver {
+                receiver
+            })) if receiver == invalid_receiver
+        ));
+    }
+
     #[motsu::test]
     fn error_when_to_is_zero(contract: Erc721Consecutive) {
         let err = contract
@@ -994,5 +1034,300 @@ mod tests {
             Error::Erc721(erc721::Error::NonexistentToken(ERC721NonexistentToken { token_id }))
             if token_id == U256::from(non_consecutive_token_id)
         ));
+
+        // After being burnt the token should not be burnt again.
+        let non_existent_token = non_consecutive_token_id;
+        let err = contract
+            ._burn(non_existent_token)
+            .expect_err("should return Error::NonexistentToken");
+
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::NonexistentToken (ERC721NonexistentToken{
+                token_id: t_id
+            })) if t_id == non_existent_token
+        ));
+    }
+
+    #[motsu::test]
+    fn safe_transfer_from(contract: Erc721Consecutive) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+        contract._mint(alice, token_id).expect("should mint a token to Alice");
+
+        contract
+            .safe_transfer_from(alice, BOB, token_id)
+            .expect("should transfer a token from Alice to Bob");
+
+        let owner = contract
+            .owner_of(token_id)
+            .expect("should return the owner of the token");
+
+        assert_eq!(owner, BOB);
+    }
+
+    #[motsu::test]
+    fn safe_transfers_from_approved_token(contract: Erc721Consecutive) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+        contract._mint(BOB, token_id).expect("should mint token to Bob");
+        contract.erc721._token_approvals.setter(token_id).set(alice);
+        contract
+            .safe_transfer_from(BOB, alice, token_id)
+            .expect("should transfer Bob's token to Alice");
+        let owner = contract
+            .owner_of(token_id)
+            .expect("should return the owner of the token");
+        assert_eq!(owner, alice);
+    }
+
+    #[motsu::test]
+    fn error_when_safe_transfer_from_incorrect_owner(
+        contract: Erc721Consecutive,
+    ) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+
+        contract._mint(alice, token_id).expect("should mint a token to Alice");
+
+        let err = contract
+            .safe_transfer_from(DAVE, BOB, token_id)
+            .expect_err("should not transfer from incorrect owner");
+
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::IncorrectOwner(ERC721IncorrectOwner {
+                sender,
+                token_id: t_id,
+                owner
+            })) if sender == DAVE && t_id == token_id && owner == alice
+        ));
+    }
+
+    #[motsu::test]
+    fn error_when_internal_safe_transfer_nonexistent_token(
+        contract: Erc721Consecutive,
+    ) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+        let err = contract
+            ._safe_transfer(alice, BOB, token_id, vec![0, 1, 2, 3].into())
+            .expect_err("should not transfer a non-existent token");
+
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::NonexistentToken(ERC721NonexistentToken {
+                token_id: t_id,
+            })) if t_id == token_id
+        ));
+    }
+
+    #[motsu::test]
+    fn error_when_safe_transfer_to_invalid_receiver(
+        contract: Erc721Consecutive,
+    ) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+        let invalid_receiver = Address::ZERO;
+
+        contract._mint(alice, token_id).expect("should mint a token to Alice");
+
+        let err = contract
+            .safe_transfer_from(alice, invalid_receiver, token_id)
+            .expect_err("should not transfer the token to invalid receiver");
+
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::InvalidReceiver(ERC721InvalidReceiver {
+                receiver
+            })) if receiver == invalid_receiver
+        ));
+
+        let owner = contract
+            .owner_of(token_id)
+            .expect("should return the owner of the token");
+        assert_eq!(alice, owner);
+    }
+
+    #[motsu::test]
+    fn safe_transfers_from_with_data(contract: Erc721Consecutive) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+        contract._mint(alice, token_id).expect("should mint a token to Alice");
+
+        contract
+            .safe_transfer_from_with_data(
+                alice,
+                BOB,
+                token_id,
+                vec![0, 1, 2, 3].into(),
+            )
+            .expect("should transfer a token from Alice to Bob");
+
+        let owner = contract
+            .owner_of(token_id)
+            .expect("should return the owner of the token");
+
+        assert_eq!(owner, BOB);
+    }
+
+    #[motsu::test]
+    fn error_when_internal_safe_transfer_to_invalid_receiver(
+        contract: Erc721Consecutive,
+    ) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+        let invalid_receiver = Address::ZERO;
+
+        contract._mint(alice, token_id).expect("should mint a token to Alice");
+
+        let err = contract
+            ._safe_transfer(
+                alice,
+                invalid_receiver,
+                token_id,
+                vec![0, 1, 2, 3].into(),
+            )
+            .expect_err("should not transfer the token to invalid receiver");
+
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::InvalidReceiver(ERC721InvalidReceiver {
+                receiver
+            })) if receiver == invalid_receiver
+        ));
+
+        let owner = contract
+            .owner_of(token_id)
+            .expect("should return the owner of the token");
+        assert_eq!(alice, owner);
+    }
+
+    #[motsu::test]
+    fn error_when_internal_safe_transfer_from_incorrect_owner(
+        contract: Erc721Consecutive,
+    ) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+
+        contract._mint(alice, token_id).expect("should mint a token to Alice");
+
+        let err = contract
+            ._safe_transfer(DAVE, BOB, token_id, vec![0, 1, 2, 3].into())
+            .expect_err("should not transfer the token from incorrect owner");
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::IncorrectOwner(ERC721IncorrectOwner {
+                sender,
+                token_id: t_id,
+                owner
+            })) if sender == DAVE && t_id == token_id && owner == alice
+        ));
+    }
+
+    #[motsu::test]
+    fn safe_mints(contract: Erc721Consecutive) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+
+        let initial_balance = contract
+            .balance_of(alice)
+            .expect("should return the balance of Alice");
+
+        contract
+            ._safe_mint(alice, token_id, vec![0, 1, 2, 3].into())
+            .expect("should mint a token for Alice");
+
+        let owner = contract
+            .owner_of(token_id)
+            .expect("should return the owner of the token");
+        assert_eq!(owner, alice);
+
+        let balance = contract
+            .balance_of(alice)
+            .expect("should return the balance of Alice");
+
+        assert_eq!(initial_balance + uint!(1_U256), balance);
+    }
+
+    #[motsu::test]
+    fn approves(contract: Erc721Consecutive) {
+        let alice = msg::sender();
+        let token_id = random_token_id();
+        contract._mint(alice, token_id).expect("should mint a token");
+        contract
+            .approve(BOB, token_id)
+            .expect("should approve Bob for operations on token");
+        assert_eq!(contract.erc721._token_approvals.get(token_id), BOB);
+    }
+
+    #[motsu::test]
+    fn error_when_approve_for_nonexistent_token(contract: Erc721Consecutive) {
+        let token_id = random_token_id();
+        let err = contract
+            .approve(BOB, token_id)
+            .expect_err("should not approve for a non-existent token");
+
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::NonexistentToken(ERC721NonexistentToken {
+                token_id: t_id
+            })) if token_id == t_id
+        ));
+    }
+
+    #[motsu::test]
+    fn error_when_approve_by_invalid_approver(contract: Erc721Consecutive) {
+        let token_id = random_token_id();
+        contract._mint(BOB, token_id).expect("should mint a token");
+
+        let err = contract
+            .approve(DAVE, token_id)
+            .expect_err("should not approve when invalid approver");
+
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::InvalidApprover(ERC721InvalidApprover {
+                approver
+            })) if approver == msg::sender()
+        ));
+    }
+
+    #[motsu::test]
+    fn approval_for_all(contract: Erc721Consecutive) {
+        let alice = msg::sender();
+        contract
+            .erc721
+            ._operator_approvals
+            .setter(alice)
+            .setter(BOB)
+            .set(false);
+
+        contract
+            .set_approval_for_all(BOB, true)
+            .expect("should approve Bob for operations on all Alice's tokens");
+        assert_eq!(contract.is_approved_for_all(alice, BOB), true);
+
+        contract.set_approval_for_all(BOB, false).expect(
+            "should disapprove Bob for operations on all Alice's tokens",
+        );
+        assert_eq!(contract.is_approved_for_all(alice, BOB), false);
+    }
+
+    #[motsu::test]
+    fn error_when_get_approved_of_nonexistent_token(
+        contract: Erc721Consecutive,
+    ) {
+        let token_id = random_token_id();
+        let err = contract
+            .get_approved(token_id)
+            .expect_err("should not return approved for a non-existent token");
+
+        assert!(matches!(
+            err,
+            Error::Erc721(erc721::Error::NonexistentToken(ERC721NonexistentToken {
+                token_id: t_id
+            })) if token_id == t_id
+        ));
     }
 }
diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs
index 119422f..9d52769 100644
--- a/contracts/src/token/erc721/mod.rs
+++ b/contracts/src/token/erc721/mod.rs
@@ -1511,9 +1511,7 @@ mod tests {
     }
 
     #[motsu::test]
-    fn error_when_safe_transfer_from_transfers_to_invalid_receiver(
-        contract: Erc721,
-    ) {
+    fn error_when_safe_transfer_to_invalid_receiver(contract: Erc721) {
         let alice = msg::sender();
         let token_id = random_token_id();
         let invalid_receiver = Address::ZERO;
@@ -2306,9 +2304,7 @@ mod tests {
     }
 
     #[motsu::test]
-    fn error_when_safe_transfer_internal_ransfers_to_invalid_receiver(
-        contract: Erc721,
-    ) {
+    fn error_when_internal_safe_transfer_to_invalid_receiver(contract: Erc721) {
         let alice = msg::sender();
         let token_id = random_token_id();
         let invalid_receiver = Address::ZERO;
@@ -2338,7 +2334,7 @@ mod tests {
     }
 
     #[motsu::test]
-    fn error_when_safe_transfer_internal_transfers_from_incorrect_owner(
+    fn error_when_internal_safe_transfer_from_incorrect_owner(
         contract: Erc721,
     ) {
         let alice = msg::sender();
@@ -2366,9 +2362,7 @@ mod tests {
     }
 
     #[motsu::test]
-    fn error_when_safe_transfer_internal_transfers_nonexistent_token(
-        contract: Erc721,
-    ) {
+    fn error_when_internal_safe_transfer_nonexistent_token(contract: Erc721) {
         let alice = msg::sender();
         let token_id = random_token_id();
         let err = contract
diff --git a/examples/access-control/Cargo.toml b/examples/access-control/Cargo.toml
index d3fdeef..cf03218 100644
--- a/examples/access-control/Cargo.toml
+++ b/examples/access-control/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -15,7 +15,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 
diff --git a/examples/basic/token/Cargo.toml b/examples/basic/token/Cargo.toml
index baee128..077980f 100644
--- a/examples/basic/token/Cargo.toml
+++ b/examples/basic/token/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
diff --git a/examples/ecdsa/Cargo.toml b/examples/ecdsa/Cargo.toml
index 2d5b3a9..7cac7e5 100644
--- a/examples/ecdsa/Cargo.toml
+++ b/examples/ecdsa/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -15,7 +15,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 
diff --git a/examples/ecdsa/src/constructor.sol b/examples/ecdsa/src/constructor.sol
deleted file mode 100644
index d10ca02..0000000
--- a/examples/ecdsa/src/constructor.sol
+++ /dev/null
@@ -1,6 +0,0 @@
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.21;
-
-contract ECDSAExample {
-    constructor() {}
-}
diff --git a/examples/erc20-permit/Cargo.toml b/examples/erc20-permit/Cargo.toml
index 29cc342..9008195 100644
--- a/examples/erc20-permit/Cargo.toml
+++ b/examples/erc20-permit/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives = { workspace = true, features = ["tiny-keccak"] }
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -17,7 +17,7 @@ mini-alloc.workspace = true
 alloy.workspace = true
 eyre.workspace = true
 tokio.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 
 [features]
 e2e = []
diff --git a/examples/erc20/Cargo.toml b/examples/erc20/Cargo.toml
index 1815680..037d3be 100644
--- a/examples/erc20/Cargo.toml
+++ b/examples/erc20/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -17,7 +17,7 @@ mini-alloc.workspace = true
 alloy.workspace = true
 eyre.workspace = true
 tokio.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 
 [features]
 e2e = []
diff --git a/examples/erc721-consecutive/Cargo.toml b/examples/erc721-consecutive/Cargo.toml
index 2c6eef2..08b7f5b 100644
--- a/examples/erc721-consecutive/Cargo.toml
+++ b/examples/erc721-consecutive/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 alloy-sol-types.workspace = true
 stylus-sdk.workspace = true
@@ -16,7 +16,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 rand.workspace = true
diff --git a/examples/erc721-metadata/Cargo.toml b/examples/erc721-metadata/Cargo.toml
index 1bc6050..62b1f29 100644
--- a/examples/erc721-metadata/Cargo.toml
+++ b/examples/erc721-metadata/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -15,7 +15,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 rand.workspace = true
diff --git a/examples/erc721/Cargo.toml b/examples/erc721/Cargo.toml
index 6f70bb7..c11254d 100644
--- a/examples/erc721/Cargo.toml
+++ b/examples/erc721/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -15,7 +15,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 rand.workspace = true
diff --git a/examples/merkle-proofs/Cargo.toml b/examples/merkle-proofs/Cargo.toml
index 60931af..e741ccb 100644
--- a/examples/merkle-proofs/Cargo.toml
+++ b/examples/merkle-proofs/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-crypto = { path = "../../lib/crypto" }
+openzeppelin-crypto.workspace = true
 alloy-primitives.workspace = true
 alloy-sol-types.workspace = true
 stylus-sdk.workspace = true
diff --git a/examples/ownable/Cargo.toml b/examples/ownable/Cargo.toml
index 13ebcbe..6ac97fa 100644
--- a/examples/ownable/Cargo.toml
+++ b/examples/ownable/Cargo.toml
@@ -7,7 +7,7 @@ publish = false
 version = "0.0.0"
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -15,7 +15,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 
diff --git a/lib/crypto/Cargo.toml b/lib/crypto/Cargo.toml
index cf119fc..777aeac 100644
--- a/lib/crypto/Cargo.toml
+++ b/lib/crypto/Cargo.toml
@@ -1,12 +1,12 @@
 [package]
 name = "openzeppelin-crypto"
-categories = ["cryptography", "algorithms", "no-std", "wasm"]
-description = "Cryptography Utilities"
+description = "Cryptographic Utilities"
 edition.workspace = true
-keywords.workspace = true
+categories = ["cryptography", "algorithms", "no-std", "wasm"]
+keywords = ["crypto", "web3", "blockchain", "merkle"]
 license.workspace = true
 repository.workspace = true
-version = "0.1.0-rc"
+version.workspace = true
 
 [dependencies]
 mini-alloc.workspace = true
diff --git a/lib/motsu-proc/Cargo.toml b/lib/motsu-proc/Cargo.toml
index 1e6bbbc..4d00902 100644
--- a/lib/motsu-proc/Cargo.toml
+++ b/lib/motsu-proc/Cargo.toml
@@ -2,7 +2,8 @@
 name = "motsu-proc"
 description = "Mostu's Procedural Macros"
 edition.workspace = true
-keywords.workspace = true
+categories = ["development-tools::testing", "cryptography::cryptocurrencies"]
+keywords = ["arbitrum", "ethereum", "stylus", "unit-tests", "tests"]
 license.workspace = true
 repository.workspace = true
 version = "0.1.0"
@@ -13,7 +14,7 @@ quote.workspace = true
 syn.workspace = true
 
 [dev-dependencies]
-motsu = { path = "../motsu" }
+motsu.workspace = true
 alloy-primitives.workspace = true
 alloy-sol-types.workspace = true
 stylus-sdk.workspace = true
diff --git a/lib/motsu/Cargo.toml b/lib/motsu/Cargo.toml
index ed66709..5f3e595 100644
--- a/lib/motsu/Cargo.toml
+++ b/lib/motsu/Cargo.toml
@@ -2,7 +2,8 @@
 name = "motsu"
 description = "Unit Testing for Stylus"
 edition.workspace = true
-keywords.workspace = true
+categories = ["development-tools::testing", "cryptography::cryptocurrencies"]
+keywords = ["arbitrum", "ethereum", "stylus", "unit-tests", "tests"]
 license.workspace = true
 repository.workspace = true
 version = "0.1.0"
@@ -12,7 +13,7 @@ const-hex.workspace = true
 once_cell.workspace = true
 tiny-keccak.workspace = true
 stylus-sdk.workspace = true
-motsu-proc = { path = "../motsu-proc" }
+motsu-proc.workspace = true
 
 [lints]
 workspace = true
diff --git a/scripts/bench.sh b/scripts/bench.sh
index 8878dae..1801d6c 100755
--- a/scripts/bench.sh
+++ b/scripts/bench.sh
@@ -5,9 +5,16 @@ MYDIR=$(realpath "$(dirname "$0")")
 cd "$MYDIR"
 cd ..
 
-NIGHTLY_TOOLCHAIN=${NIGHTLY_TOOLCHAIN:-nightly}
+NIGHTLY_TOOLCHAIN=${NIGHTLY_TOOLCHAIN:-nightly-2024-01-01}
 cargo +"$NIGHTLY_TOOLCHAIN" build --release --target wasm32-unknown-unknown -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort
 
 export RPC_URL=http://localhost:8547
-cargo run --release -p benches
+
+# No need to compile benchmarks with `--release`
+# since this only runs the benchmarking code and the contracts have already been compiled with `--release`
+cargo run -p benches
+
+echo "NOTE: To measure non cached contract's gas usage correctly,
+ benchmarks should run on a clean instance of the nitro test node."
+echo
 echo "Finished running benches!"


Test patch:
diff --git a/lib/e2e-proc/Cargo.toml b/lib/e2e-proc/Cargo.toml
index c89b39f..aaa76ad 100644
--- a/lib/e2e-proc/Cargo.toml
+++ b/lib/e2e-proc/Cargo.toml
@@ -2,10 +2,11 @@
 name = "e2e-proc"
 description = "End-to-end Testing Procedural Macros"
 version = "0.1.0"
+categories = ["development-tools::testing", "cryptography::cryptocurrencies"]
+keywords = ["arbitrum", "ethereum", "stylus", "integration-testing", "tests"]
 authors.workspace = true
 edition.workspace = true
 license.workspace = true
-keywords.workspace = true
 repository.workspace = true
 
 [dependencies]
diff --git a/lib/e2e/Cargo.toml b/lib/e2e/Cargo.toml
index 6d6cf93..84a9c6d 100644
--- a/lib/e2e/Cargo.toml
+++ b/lib/e2e/Cargo.toml
@@ -2,10 +2,11 @@
 name = "e2e"
 description = "End-to-end Testing for Stylus"
 version = "0.1.0"
+categories = ["development-tools::testing", "cryptography::cryptocurrencies"]
+keywords = ["arbitrum", "ethereum", "stylus", "integration-testing", "tests"]
 authors.workspace = true
 edition.workspace = true
 license.workspace = true
-keywords.workspace = true
 repository.workspace = true
 
 [dependencies]
@@ -15,7 +16,7 @@ eyre.workspace = true
 regex.workspace = true
 once_cell.workspace = true
 koba.workspace = true
-e2e-proc = { path = "../e2e-proc" }
+e2e-proc.workspace = true
 toml = "0.8.13"
 
 [lints]
diff --git a/lib/e2e/src/account.rs b/lib/e2e/src/account.rs
index 5e0e955..83d4cd2 100644
--- a/lib/e2e/src/account.rs
+++ b/lib/e2e/src/account.rs
@@ -13,6 +13,8 @@ use crate::{
     system::{fund_account, Wallet, RPC_URL_ENV_VAR_NAME},
 };
 
+const DEFAULT_FUNDING_ETH: u32 = 100;
+
 /// Type that corresponds to a test account.
 #[derive(Clone, Debug)]
 pub struct Account {
@@ -23,7 +25,7 @@ pub struct Account {
 }
 
 impl Account {
-    /// Create a new account.
+    /// Create a new account with a default funding of [`DEFAULT_FUNDING_ETH`].
     ///
     /// # Errors
     ///
@@ -103,7 +105,7 @@ impl AccountFactory {
 
         let signer = PrivateKeySigner::random();
         let addr = signer.address();
-        fund_account(addr, "100")?;
+        fund_account(addr, DEFAULT_FUNDING_ETH)?;
 
         let rpc_url = std::env::var(RPC_URL_ENV_VAR_NAME)
             .expect("failed to load RPC_URL var from env")
diff --git a/lib/e2e/src/deploy.rs b/lib/e2e/src/deploy.rs
index a307472..e65aef6 100644
--- a/lib/e2e/src/deploy.rs
+++ b/lib/e2e/src/deploy.rs
@@ -1,3 +1,5 @@
+use std::path::Path;
+
 use alloy::{rpc::types::TransactionReceipt, sol_types::SolConstructor};
 use koba::config::Deploy;
 
@@ -45,11 +47,13 @@ impl Deployer {
         let pkg = Crate::new()?;
         let wasm_path = pkg.wasm;
         let sol_path = pkg.manifest_dir.join("src/constructor.sol");
+        let sol =
+            if Path::new(&sol_path).exists() { Some(sol_path) } else { None };
 
         let config = Deploy {
             generate_config: koba::config::Generate {
                 wasm: wasm_path.clone(),
-                sol: Some(sol_path),
+                sol,
                 args: self.ctr_args,
                 legacy: false,
             },
diff --git a/lib/e2e/src/system.rs b/lib/e2e/src/system.rs
index aef9ac3..5d90cd8 100644
--- a/lib/e2e/src/system.rs
+++ b/lib/e2e/src/system.rs
@@ -61,7 +61,7 @@ pub fn provider() -> Provider {
 }
 
 /// Send `amount` eth to `address` in the nitro-tesnode.
-pub fn fund_account(address: Address, amount: &str) -> eyre::Result<()> {
+pub fn fund_account(address: Address, amount: u32) -> eyre::Result<()> {
     let node_script = get_node_path()?.join("test-node.bash");
     if !node_script.exists() {
         bail!("Test nitro node wasn't setup properly. Try to setup it first with `./scripts/nitro-testnode.sh -i -d`")
@@ -73,7 +73,7 @@ pub fn fund_account(address: Address, amount: &str) -> eyre::Result<()> {
         .arg("--to")
         .arg(format!("address_{address}"))
         .arg("--ethamount")
-        .arg(amount)
+        .arg(amount.to_string())
         .output()?;
 
     if !output.status.success() {
diff --git a/scripts/nitro-testnode.sh b/scripts/nitro-testnode.sh
index 815323c..4e80737 100755
--- a/scripts/nitro-testnode.sh
+++ b/scripts/nitro-testnode.sh
@@ -18,7 +18,14 @@ do
       shift
       ;;
     -q|--quit)
-      docker container stop $(docker container ls -q --filter name=nitro-testnode)
+      NITRO_CONTAINERS=$(docker container ls -q --filter name=nitro-testnode)
+
+      if [ -z "$NITRO_CONTAINERS" ]; then
+          echo "No nitro-testnode containers running"
+      else
+          docker container stop $NITRO_CONTAINERS || exit
+      fi
+
       exit 0
       ;;
     *)
@@ -43,8 +50,8 @@ then
 
   git clone --recurse-submodules https://github.com/OffchainLabs/nitro-testnode.git
   cd ./nitro-testnode || exit
-  # `release` branch.
-  git checkout 8cb6b84e31909157d431e7e4af9fb83799443e00 || exit
+  git pull origin release --recurse-submodules
+  git checkout d4244cd5c2cb56ca3d11c23478ef9642f8ebf472 || exit
 
   ./test-node.bash --no-run --init || exit
 fi