diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 1ecd432d..17b354f1 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -10,18 +10,27 @@ on: branches: [ main, v* ] jobs: - check-links: - name: Check Links + check-links-md: + name: Check Markdown Links runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Link Checker - uses: lycheeverse/lychee-action@v1 + uses: lycheeverse/lychee-action@v2 with: args: --no-progress './**/*.md' fail: true + check-links-adoc: + name: Check AsciiDoc Links + runs-on: ubuntu-latest + # We only run the AsciiDoc link checker on v* branches because: + # 1. The main branch may contain documentation that is not yet published. + if: startsWith(github.ref, 'refs/heads/v') || startsWith(github.base_ref, 'v') + steps: + - uses: actions/checkout@v4 + - name: Run linkspector uses: umbrelladocs/action-linkspector@v1 with: diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 708a0862..a8ee0f60 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -110,4 +110,4 @@ jobs: - name: Checkout Actions Repository uses: actions/checkout@v4 - name: Check spelling of files in the workspace - uses: crate-ci/typos@v1.24.1 + uses: crate-ci/typos@v1.26.0 diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index f1a5880b..cebd0379 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -43,7 +43,7 @@ jobs: - name: install solc run: | - curl -LO https://github.com/ethereum/solidity/releases/download/v0.8.21/solc-static-linux + curl -LO https://github.com/ethereum/solidity/releases/download/v0.8.24/solc-static-linux sudo mv solc-static-linux /usr/bin/solc sudo chmod a+x /usr/bin/solc diff --git a/.github/workflows/gas-bench.yml b/.github/workflows/gas-bench.yml index 7d572fc4..a14bf25c 100644 --- a/.github/workflows/gas-bench.yml +++ b/.github/workflows/gas-bench.yml @@ -31,9 +31,12 @@ 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.21/solc-static-linux + curl -LO https://github.com/ethereum/solidity/releases/download/v0.8.24/solc-static-linux sudo mv solc-static-linux /usr/bin/solc sudo chmod a+x /usr/bin/solc diff --git a/Cargo.lock b/Cargo.lock index 151e9aa8..835134ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,7 +12,6 @@ dependencies = [ "eyre", "mini-alloc", "openzeppelin-stylus", - "stylus-proc", "stylus-sdk", "tokio", ] @@ -799,7 +798,6 @@ dependencies = [ "alloy-primitives", "mini-alloc", "openzeppelin-stylus", - "stylus-proc", "stylus-sdk", ] @@ -822,6 +820,7 @@ dependencies = [ "alloy-primitives", "e2e", "eyre", + "futures", "keccak-const", "koba", "openzeppelin-stylus", @@ -1270,7 +1269,6 @@ dependencies = [ "eyre", "mini-alloc", "openzeppelin-stylus", - "stylus-proc", "stylus-sdk", "tokio", ] @@ -1522,7 +1520,6 @@ dependencies = [ "eyre", "mini-alloc", "openzeppelin-stylus", - "stylus-proc", "stylus-sdk", "tokio", ] @@ -1537,7 +1534,6 @@ dependencies = [ "eyre", "mini-alloc", "openzeppelin-stylus", - "stylus-proc", "stylus-sdk", "tokio", ] @@ -1554,7 +1550,6 @@ dependencies = [ "mini-alloc", "openzeppelin-stylus", "rand", - "stylus-proc", "stylus-sdk", "tokio", ] @@ -1570,7 +1565,6 @@ dependencies = [ "mini-alloc", "openzeppelin-stylus", "rand", - "stylus-proc", "stylus-sdk", "tokio", ] @@ -1586,7 +1580,6 @@ dependencies = [ "mini-alloc", "openzeppelin-stylus", "rand", - "stylus-proc", "stylus-sdk", "tokio", ] @@ -2366,7 +2359,6 @@ dependencies = [ "alloy-sol-types", "mini-alloc", "openzeppelin-crypto", - "stylus-proc", "stylus-sdk", ] @@ -2432,7 +2424,6 @@ dependencies = [ "motsu", "proc-macro2", "quote", - "stylus-proc", "stylus-sdk", "syn 2.0.68", ] @@ -2596,7 +2587,6 @@ dependencies = [ "motsu", "openzeppelin-stylus-proc", "rand", - "stylus-proc", "stylus-sdk", ] @@ -2620,7 +2610,6 @@ dependencies = [ "eyre", "mini-alloc", "openzeppelin-stylus", - "stylus-proc", "stylus-sdk", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index f308ddea..244e4355 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,6 @@ resolver = "2" authors = ["OpenZeppelin"] edition = "2021" license = "MIT" -keywords = ["arbitrum", "ethereum", "stylus"] repository = "https://github.com/OpenZeppelin/rust-contracts-stylus" version = "0.1.0" @@ -64,7 +63,6 @@ all = "warn" [workspace.dependencies] # stylus-related stylus-sdk = { version = "=0.6.0", default-features = false } -stylus-proc = { version = "=0.6.0", default-features = false } mini-alloc = "0.4.2" alloy = { version = "=0.1.4", features = [ @@ -95,6 +93,7 @@ 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"] } diff --git a/GUIDELINES.md b/GUIDELINES.md index 6f41be09..2772b3a5 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -131,7 +131,7 @@ Use 1-word names for function parameters or variables that have larger scopes: ```rust pub fn commit(repo: &Repository, sig: &Signature) -> Result { -... + ... } ``` @@ -207,6 +207,41 @@ Make sure all tests are passing with: $ cargo test --all-features +### Running end-to-end tests + +In order to run end-to-end (e2e) tests you need to have a specific nightly toolchain. +"Nightly" is necessary to use optimization compiler flags and have contract wasm small enough to be eligible for +deployment. + +Run the following commands to install the necessary toolchain: + +```shell +rustup install nightly-2024-01-01 +rustup component add rust-src +``` + +Also, you should have the cargo stylus tool: + +```shell +cargo install cargo-stylus +``` + +Since most of the e2e tests use [koba](https://github.com/OpenZeppelin/koba) for deploying contracts, you need to +[install](https://docs.soliditylang.org/en/latest/installing-solidity.html#) the solidity compiler (`v0.8.24`). + +To run e2e tests, you need to have a local nitro test node up and running. +Run the following command and wait till script exit successfully: + +```shell +./scripts/nitro-testnode.sh -i -d +``` + +Then you will be able to run e2e tests: + +```shell +./scripts/e2e-tests.sh +``` + ### Checking the docs If you make documentation changes, you may want to check whether there are any @@ -279,12 +314,15 @@ conventions that must be followed. - Custom errors should be declared following the [EIP-6093] rationale whenever reasonable. Also, consider the following: - - The domain prefix should be picked in the following order: - 1. Use `ERC` if the error is a violation of an ERC specification. - 2. Use the name of the underlying component where it belongs (eg. - `Governor`, `ECDSA`, or `Timelock`). + - The domain prefix should be picked in the following order: + 1. Use `ERC` if the error is a violation of an ERC specification. + 2. Use the name of the underlying component where it belongs (eg. + `Governor`, `ECDSA`, or `Timelock`). [The Rust Style Guide]: https://doc.rust-lang.org/nightly/style-guide/ + [EIP-6093]: https://eips.ethereum.org/EIPS/eip-6093 + [Semantic versioning]: https://semver.org/spec/v2.0.0.html + [Conventional Commits]: https://www.conventionalcommits.org/en/v1.0.0/ diff --git a/README.md b/README.md index 696fe221..eed2be55 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,11 @@ - [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 @@ -50,7 +53,7 @@ sol_storage! { #[public] #[inherit(Erc20)] -impl Erc20Example { } +impl Erc20Example {} ``` For a more complex display of what this library offers, refer to our @@ -62,8 +65,6 @@ For a full example that includes deploying and querying a contract, see the For more information on what this library will include in the future, see our [roadmap]. -[specify a git dependency]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories -[examples]: ./examples [basic]: ./examples/basic [roadmap]: https://github.com/OpenZeppelin/rust-contracts-stylus/milestone/2 @@ -81,4 +82,5 @@ Refer to our [Security Policy](SECURITY.md) for more details. ## License -OpenZeppelin Contracts for Stylus is released under the [MIT License](./LICENSE). +OpenZeppelin Contracts for Stylus is released under +the [MIT License](./LICENSE). diff --git a/benches/Cargo.toml b/benches/Cargo.toml index f16effd4..5cb17d8e 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -11,6 +11,7 @@ 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.workspace = true diff --git a/benches/src/access_control.rs b/benches/src/access_control.rs index 7c9b7978..12ccfaa8 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 { +pub async fn bench() -> eyre::Result { + 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> { 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 { .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 { (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::>>() } -async fn deploy(account: &Account) -> Address { +async fn deploy( + account: &Account, + cache_opt: CacheOpt, +) -> eyre::Result
{ 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 22f2aff0..bcf63435 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 { +pub async fn bench() -> eyre::Result { + 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> { 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 { .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 { (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::>>() } -async fn deploy(account: &Account) -> Address { +async fn deploy( + account: &Account, + cache_opt: CacheOpt, +) -> eyre::Result
{ 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 b5dc4bb2..b00729a1 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 { +pub async fn bench() -> eyre::Result { + 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> { 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 { 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 { (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::>>() } -async fn deploy(account: &Account) -> Address { +async fn deploy( + account: &Account, + cache_opt: CacheOpt, +) -> eyre::Result
{ 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 85a37ca0..c884bcdb 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>>; -fn get_l2_gas_used(receipt: &ArbTxReceipt) -> eyre::Result { - let l2_gas = receipt.gas_used; - let arb_fields: ArbOtherFields = receipt.other.deserialize_as()?; - let l1_gas = arb_fields.gas_used_for_l1.to::(); - Ok(l2_gas - l1_gas) -} - async fn deploy( account: &Account, contract_name: &str, args: Option, -) -> Address { + cache_opt: CacheOpt, +) -> eyre::Result
{ 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 { + std::env::var(name).wrap_err(format!("failed to load {name}")) } diff --git a/benches/src/main.rs b/benches/src/main.rs index cd4e0e38..52278d77 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 92a7d55e..fd986520 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 { +pub async fn bench() -> eyre::Result { + 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> { let alice = Account::new().await?; let alice_wallet = ProviderBuilder::new() .network::() @@ -65,7 +84,8 @@ pub async fn bench() -> eyre::Result { .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 { 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::>>() } -async fn deploy(account: &Account) -> Address { - crate::deploy(account, "merkle-proofs", None).await +async fn deploy( + account: &Account, + cache_opt: CacheOpt, +) -> eyre::Result
{ + crate::deploy(account, "merkle-proofs", None, cache_opt).await } diff --git a/benches/src/report.rs b/benches/src/report.rs index 3a1184b2..49b5fe70 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 { + 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, + functions_cached: Vec, } -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.functions.push(fn_report); + Ok(self) } - pub fn add(mut self, receipt: (&str, ArbTxReceipt)) -> eyre::Result { - 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.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); +pub struct BenchmarkReport(Vec); -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: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:10}")?; + let full_sig = format!("{prefix}{sig}"); + writeln!( + f, + "| {full_sig:width2$} | {gas:>width3$} |" + )?; } } Ok(()) } } + +fn get_l2_gas_used(receipt: &ArbTxReceipt) -> eyre::Result { + let l2_gas = receipt.gas_used; + let arb_fields: ArbOtherFields = receipt.other.deserialize_as()?; + let l1_gas = arb_fields.gas_used_for_l1.to::(); + Ok(l2_gas - l1_gas) +} diff --git a/contracts-proc/Cargo.toml b/contracts-proc/Cargo.toml index 4f54179a..85b40c37 100644 --- a/contracts-proc/Cargo.toml +++ b/contracts-proc/Cargo.toml @@ -4,11 +4,11 @@ description = "Procedural macros for OpenZeppelin Stylus contracts" authors.workspace = true edition.workspace = true license.workspace = true -keywords.workspace = true repository.workspace = true +keywords = ["arbitrum", "ethereum", "stylus", "smart-contracts", "standards"] +categories = ["cryptography::cryptocurrencies", "no-std", "wasm"] version = "0.1.0" - [dependencies] proc-macro2.workspace = true quote.workspace = true diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index bcd31bc2..269161b2 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,9 +1,9 @@ [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.workspace = true @@ -15,7 +15,6 @@ alloy-sol-macro.workspace = true alloy-sol-macro-expander.workspace = true alloy-sol-macro-input.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true keccak-const.workspace = true openzeppelin-stylus-proc.workspace = true diff --git a/contracts/src/access/control.rs b/contracts/src/access/control.rs index 7d5a7cf5..1ef99735 100644 --- a/contracts/src/access/control.rs +++ b/contracts/src/access/control.rs @@ -42,10 +42,9 @@ //! this role. use alloy_primitives::{Address, B256}; use alloy_sol_types::sol; -use stylus_proc::SolidityError; use stylus_sdk::{ evm, msg, - stylus_proc::{public, sol_storage}, + stylus_proc::{public, sol_storage, SolidityError}, }; sol! { diff --git a/contracts/src/access/ownable.rs b/contracts/src/access/ownable.rs index 01a56369..157457fd 100644 --- a/contracts/src/access/ownable.rs +++ b/contracts/src/access/ownable.rs @@ -10,10 +10,9 @@ //! to the owner. use alloy_primitives::Address; use alloy_sol_types::sol; -use stylus_proc::SolidityError; use stylus_sdk::{ evm, msg, - stylus_proc::{public, sol_storage}, + stylus_proc::{public, sol_storage, SolidityError}, }; sol! { diff --git a/contracts/src/token/erc20/extensions/capped.rs b/contracts/src/token/erc20/extensions/capped.rs index 40fdea92..a971900b 100644 --- a/contracts/src/token/erc20/extensions/capped.rs +++ b/contracts/src/token/erc20/extensions/capped.rs @@ -7,7 +7,7 @@ use alloy_primitives::U256; use alloy_sol_types::sol; -use stylus_proc::{public, sol_storage, SolidityError}; +use stylus_sdk::stylus_proc::{public, sol_storage, SolidityError}; sol! { /// Indicates an error related to the operation that failed diff --git a/contracts/src/token/erc20/extensions/metadata.rs b/contracts/src/token/erc20/extensions/metadata.rs index 0e9902b7..6cc10141 100644 --- a/contracts/src/token/erc20/extensions/metadata.rs +++ b/contracts/src/token/erc20/extensions/metadata.rs @@ -4,7 +4,7 @@ use alloc::string::String; use alloy_primitives::FixedBytes; use openzeppelin_stylus_proc::interface_id; -use stylus_proc::{public, sol_storage}; +use stylus_sdk::stylus_proc::{public, sol_storage}; use crate::utils::introspection::erc165::IErc165; diff --git a/contracts/src/token/erc20/extensions/permit.rs b/contracts/src/token/erc20/extensions/permit.rs index 1cd597ae..1b58e948 100644 --- a/contracts/src/token/erc20/extensions/permit.rs +++ b/contracts/src/token/erc20/extensions/permit.rs @@ -11,8 +11,12 @@ //! and thus is not required to hold Ether at all. use alloy_primitives::{b256, keccak256, Address, B256, U256}; use alloy_sol_types::{sol, SolType}; -use stylus_proc::{public, sol_storage, SolidityError}; -use stylus_sdk::{block, prelude::StorageType, storage::TopLevelStorage}; +use stylus_sdk::{ + block, + prelude::StorageType, + storage::TopLevelStorage, + stylus_proc::{public, sol_storage, SolidityError}, +}; use crate::{ token::erc20::{self, Erc20, IErc20}, diff --git a/contracts/src/token/erc20/mod.rs b/contracts/src/token/erc20/mod.rs index 6916c466..19fba1ea 100644 --- a/contracts/src/token/erc20/mod.rs +++ b/contracts/src/token/erc20/mod.rs @@ -7,11 +7,10 @@ use alloy_primitives::{Address, FixedBytes, U256}; use alloy_sol_types::sol; use openzeppelin_stylus_proc::interface_id; -use stylus_proc::SolidityError; use stylus_sdk::{ call::MethodError, evm, msg, - stylus_proc::{public, sol_storage}, + stylus_proc::{public, sol_storage, SolidityError}, }; use crate::utils::introspection::erc165::{Erc165, IErc165}; diff --git a/contracts/src/token/erc721/extensions/consecutive.rs b/contracts/src/token/erc721/extensions/consecutive.rs index 2788ccad..302cb56f 100644 --- a/contracts/src/token/erc721/extensions/consecutive.rs +++ b/contracts/src/token/erc721/extensions/consecutive.rs @@ -26,8 +26,12 @@ use alloc::vec; use alloy_primitives::{uint, Address, U256}; use alloy_sol_types::sol; -use stylus_proc::{public, sol_storage, SolidityError}; -use stylus_sdk::{abi::Bytes, evm, msg, prelude::TopLevelStorage}; +use stylus_sdk::{ + abi::Bytes, + evm, msg, + prelude::TopLevelStorage, + stylus_proc::{public, sol_storage, SolidityError}, +}; use crate::{ token::{ @@ -802,11 +806,13 @@ mod tests { ERC721ExceededMaxBatchMint, Erc721Consecutive, Error, U96, }, tests::random_token_id, - ERC721InvalidReceiver, ERC721NonexistentToken, IErc721, + ERC721IncorrectOwner, ERC721InvalidApprover, ERC721InvalidReceiver, + ERC721InvalidSender, ERC721NonexistentToken, IErc721, }, }; const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526"); + const DAVE: Address = address!("0BB78F7e7132d1651B4Fd884B7624394e92156F1"); fn init( contract: &mut Erc721Consecutive, @@ -857,6 +863,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 @@ -993,5 +1036,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/extensions/enumerable.rs b/contracts/src/token/erc721/extensions/enumerable.rs index aa513764..3511489e 100644 --- a/contracts/src/token/erc721/extensions/enumerable.rs +++ b/contracts/src/token/erc721/extensions/enumerable.rs @@ -12,7 +12,7 @@ use alloy_primitives::{uint, Address, FixedBytes, U256}; use alloy_sol_types::sol; use openzeppelin_stylus_proc::interface_id; -use stylus_proc::{public, sol_storage, SolidityError}; +use stylus_sdk::stylus_proc::{public, sol_storage, SolidityError}; use crate::{ token::{erc721, erc721::IErc721}, diff --git a/contracts/src/token/erc721/extensions/metadata.rs b/contracts/src/token/erc721/extensions/metadata.rs index 35b8fd2d..c1e95e07 100644 --- a/contracts/src/token/erc721/extensions/metadata.rs +++ b/contracts/src/token/erc721/extensions/metadata.rs @@ -3,7 +3,8 @@ use alloc::string::{String, ToString}; use alloy_primitives::{FixedBytes, U256}; -use stylus_proc::{public, sol_storage}; +use openzeppelin_stylus_proc::interface_id; +use stylus_sdk::stylus_proc::{public, sol_storage}; use crate::{ token::erc721::{Error, IErc721}, @@ -21,6 +22,7 @@ sol_storage! { } /// Interface for the optional metadata functions from the ERC-721 standard. +#[interface_id] pub trait IErc721Metadata { /// Returns the token collection name. /// @@ -111,3 +113,17 @@ impl Erc721Metadata { Ok(token_uri) } } + +#[cfg(all(test, feature = "std"))] +mod tests { + // use crate::token::erc721::extensions::{Erc721Metadata, IErc721Metadata}; + + // TODO: IErc721Metadata should be refactored to have same api as solidity + // has: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4764ea50750d8bda9096e833706beba86918b163/contracts/token/ERC721/extensions/IERC721Metadata.sol#L12 + // [motsu::test] + // fn interface_id() { + // let actual = ::INTERFACE_ID; + // let expected = 0x5b5e139f; + // assert_eq!(actual, expected); + // } +} diff --git a/contracts/src/token/erc721/extensions/uri_storage.rs b/contracts/src/token/erc721/extensions/uri_storage.rs index c5f98f38..644a3dc2 100644 --- a/contracts/src/token/erc721/extensions/uri_storage.rs +++ b/contracts/src/token/erc721/extensions/uri_storage.rs @@ -5,8 +5,7 @@ use alloc::string::String; use alloy_primitives::U256; use alloy_sol_types::sol; -use stylus_proc::sol_storage; -use stylus_sdk::evm; +use stylus_sdk::{evm, stylus_proc::sol_storage}; use crate::token::erc721::{extensions::Erc721Metadata, Error, IErc721}; @@ -107,8 +106,7 @@ impl Erc721UriStorage { #[cfg(all(test, feature = "std"))] mod tests { use alloy_primitives::U256; - use stylus_proc::sol_storage; - use stylus_sdk::msg; + use stylus_sdk::{msg, stylus_proc::sol_storage}; use super::Erc721UriStorage; use crate::token::erc721::{extensions::Erc721Metadata, Erc721}; diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index 1101cb8b..a3510568 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -1520,9 +1520,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; @@ -2315,9 +2313,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; @@ -2347,7 +2343,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(); @@ -2375,9 +2371,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/contracts/src/utils/cryptography/ecdsa.rs b/contracts/src/utils/cryptography/ecdsa.rs index b6b25319..05aed2b5 100644 --- a/contracts/src/utils/cryptography/ecdsa.rs +++ b/contracts/src/utils/cryptography/ecdsa.rs @@ -6,10 +6,10 @@ use alloc::vec::Vec; use alloy_primitives::{address, uint, Address, B256, U256}; use alloy_sol_types::{sol, SolType}; -use stylus_proc::SolidityError; use stylus_sdk::{ call::{self, Call, MethodError}, storage::TopLevelStorage, + stylus_proc::SolidityError, }; use crate::utils::cryptography::ecdsa; diff --git a/contracts/src/utils/metadata.rs b/contracts/src/utils/metadata.rs index de3e83f8..7d904dbb 100644 --- a/contracts/src/utils/metadata.rs +++ b/contracts/src/utils/metadata.rs @@ -1,7 +1,7 @@ //! Common Metadata Smart Contract. use alloc::string::String; -use stylus_proc::{public, sol_storage}; +use stylus_sdk::stylus_proc::{public, sol_storage}; sol_storage! { /// Metadata of the token. diff --git a/contracts/src/utils/nonces.rs b/contracts/src/utils/nonces.rs index da02be73..75256517 100644 --- a/contracts/src/utils/nonces.rs +++ b/contracts/src/utils/nonces.rs @@ -4,7 +4,7 @@ use alloy_primitives::{uint, Address, U256}; use alloy_sol_types::sol; -use stylus_proc::{public, sol_storage, SolidityError}; +use stylus_sdk::stylus_proc::{public, sol_storage, SolidityError}; const ONE: U256 = uint!(1_U256); diff --git a/contracts/src/utils/pausable.rs b/contracts/src/utils/pausable.rs index 5f973992..1a2ed5ae 100644 --- a/contracts/src/utils/pausable.rs +++ b/contracts/src/utils/pausable.rs @@ -15,8 +15,10 @@ //! You should expose them manually in your contract's abi. use alloy_sol_types::sol; -use stylus_proc::{public, sol_storage, SolidityError}; -use stylus_sdk::{evm, msg}; +use stylus_sdk::{ + evm, msg, + stylus_proc::{public, sol_storage, SolidityError}, +}; sol! { /// Emitted when pause is triggered by `account`. diff --git a/contracts/src/utils/structs/bitmap.rs b/contracts/src/utils/structs/bitmap.rs index 35489334..b36c0037 100644 --- a/contracts/src/utils/structs/bitmap.rs +++ b/contracts/src/utils/structs/bitmap.rs @@ -14,7 +14,7 @@ //! //! [merkle-distributor]: https://github.com/Uniswap/merkle-distributor/blob/master/contracts/MerkleDistributor.sol use alloy_primitives::{uint, U256}; -use stylus_proc::sol_storage; +use stylus_sdk::stylus_proc::sol_storage; const ONE: U256 = uint!(0x1_U256); const HEX_FF: U256 = uint!(0xff_U256); diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 05060ec6..1bcc4ea4 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -9,3 +9,4 @@ * xref:access-control.adoc[Access Control] * xref:crypto.adoc[Cryptography] +* xref:utilities.adoc[Utilities] diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc new file mode 100644 index 00000000..0d6bc569 --- /dev/null +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -0,0 +1,43 @@ += Utilities + +The OpenZeppelin Stylus Contracts provides a ton of useful utilities that you can use in your project. +For a complete list, check out the https://docs.rs/openzeppelin-stylus/0.1.0/openzeppelin_stylus/utils/index.html[API Reference]. +Here are some of the more popular ones. + +[[introspection]] +== Introspection + +It's frequently helpful to know whether a contract supports an interface you'd like to use. +https://eips.ethereum.org/EIPS/eip-165[`ERC-165`] is a standard that helps do runtime interface detection. +Contracts for Stylus provides helpers for implementing ERC-165 in your contracts: + +* https://docs.rs/openzeppelin-stylus/0.1.0/openzeppelin_stylus/utils/introspection/erc165/trait.IErc165.html[`IERC165`] — this is the ERC-165 trait that defines https://docs.rs/openzeppelin-stylus/0.1.0/openzeppelin_stylus/utils/introspection/erc165/trait.IErc165.html#tymethod.supports_interface[`supportsInterface`]. In order to implement ERC-165 interface detection, you should manually expose https://docs.rs/openzeppelin-stylus/0.1.0/openzeppelin_stylus/utils/introspection/erc165/trait.IErc165.html#tymethod.supports_interface[`supportsInterface`] function in your contract. + +[source,rust] +---- +sol_storage! { + #[entrypoint] + struct Erc721Example { + #[borrow] + Erc721 erc721; + } +} + +#[public] +#[inherit(Erc721)] +impl Erc721Example { + pub fn supports_interface(interface_id: FixedBytes<4>) -> bool { + Erc721::supports_interface(interface_id) + } +} + +---- + +[[structures]] +== Structures + +Some use cases require more powerful data structures than arrays and mappings offered natively in alloy and the Stylus sdk. +Contracts for Stylus provides these libraries for enhanced data structure management: + +- https://docs.rs/openzeppelin-stylus/0.1.0/openzeppelin_stylus/utils/structs/bitmap/index.html[`BitMaps`]: Store packed booleans in storage. +- https://docs.rs/openzeppelin-stylus/0.1.0/openzeppelin_stylus/utils/structs/checkpoints/index.html[`Checkpoints`]: Checkpoint values with built-in lookups. diff --git a/examples/access-control/Cargo.toml b/examples/access-control/Cargo.toml index cf032181..37cdff42 100644 --- a/examples/access-control/Cargo.toml +++ b/examples/access-control/Cargo.toml @@ -10,7 +10,6 @@ version.workspace = true openzeppelin-stylus.workspace = true alloy-primitives.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true [dev-dependencies] diff --git a/examples/basic/token/Cargo.toml b/examples/basic/token/Cargo.toml index 077980f9..27998d9e 100644 --- a/examples/basic/token/Cargo.toml +++ b/examples/basic/token/Cargo.toml @@ -10,7 +10,6 @@ version.workspace = true openzeppelin-stylus.workspace = true alloy-primitives.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true [features] diff --git a/examples/ecdsa/Cargo.toml b/examples/ecdsa/Cargo.toml index 7cac7e54..b0c9b012 100644 --- a/examples/ecdsa/Cargo.toml +++ b/examples/ecdsa/Cargo.toml @@ -10,7 +10,6 @@ version.workspace = true openzeppelin-stylus.workspace = true alloy-primitives.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true [dev-dependencies] diff --git a/examples/ecdsa/src/constructor.sol b/examples/ecdsa/src/constructor.sol deleted file mode 100644 index d10ca026..00000000 --- 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 90081954..a17c2f3e 100644 --- a/examples/erc20-permit/Cargo.toml +++ b/examples/erc20-permit/Cargo.toml @@ -10,7 +10,6 @@ version.workspace = true openzeppelin-stylus.workspace = true alloy-primitives = { workspace = true, features = ["tiny-keccak"] } stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true [dev-dependencies] diff --git a/examples/erc20/Cargo.toml b/examples/erc20/Cargo.toml index 037d3be9..3f89418b 100644 --- a/examples/erc20/Cargo.toml +++ b/examples/erc20/Cargo.toml @@ -10,7 +10,6 @@ version.workspace = true openzeppelin-stylus.workspace = true alloy-primitives.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true [dev-dependencies] diff --git a/examples/erc721-consecutive/Cargo.toml b/examples/erc721-consecutive/Cargo.toml index 08b7f5b4..4616b73a 100644 --- a/examples/erc721-consecutive/Cargo.toml +++ b/examples/erc721-consecutive/Cargo.toml @@ -11,7 +11,6 @@ openzeppelin-stylus.workspace = true alloy-primitives.workspace = true alloy-sol-types.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true [dev-dependencies] diff --git a/examples/erc721-metadata/Cargo.toml b/examples/erc721-metadata/Cargo.toml index 62b1f298..50c22793 100644 --- a/examples/erc721-metadata/Cargo.toml +++ b/examples/erc721-metadata/Cargo.toml @@ -10,7 +10,6 @@ version.workspace = true openzeppelin-stylus.workspace = true alloy-primitives.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true [dev-dependencies] diff --git a/examples/erc721/Cargo.toml b/examples/erc721/Cargo.toml index c11254dc..1f6f916d 100644 --- a/examples/erc721/Cargo.toml +++ b/examples/erc721/Cargo.toml @@ -10,7 +10,6 @@ version.workspace = true openzeppelin-stylus.workspace = true alloy-primitives.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true [dev-dependencies] diff --git a/examples/merkle-proofs/Cargo.toml b/examples/merkle-proofs/Cargo.toml index e741ccbe..02c229e4 100644 --- a/examples/merkle-proofs/Cargo.toml +++ b/examples/merkle-proofs/Cargo.toml @@ -11,7 +11,6 @@ openzeppelin-crypto.workspace = true alloy-primitives.workspace = true alloy-sol-types.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true [features] diff --git a/examples/merkle-proofs/src/lib.rs b/examples/merkle-proofs/src/lib.rs index 2fab9264..a442fb16 100644 --- a/examples/merkle-proofs/src/lib.rs +++ b/examples/merkle-proofs/src/lib.rs @@ -8,10 +8,10 @@ use openzeppelin_crypto::{ merkle::{self, Verifier}, KeccakBuilder, }; -use stylus_proc::SolidityError; use stylus_sdk::{ alloy_sol_types::sol, prelude::{entrypoint, public, sol_storage}, + stylus_proc::SolidityError, }; #[global_allocator] diff --git a/examples/ownable/Cargo.toml b/examples/ownable/Cargo.toml index 6ac97fa3..f60e1075 100644 --- a/examples/ownable/Cargo.toml +++ b/examples/ownable/Cargo.toml @@ -10,7 +10,6 @@ version = "0.0.0" openzeppelin-stylus.workspace = true alloy-primitives.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true mini-alloc.workspace = true [dev-dependencies] diff --git a/lib/crypto/Cargo.toml b/lib/crypto/Cargo.toml index 81a44fd8..777aeaca 100644 --- a/lib/crypto/Cargo.toml +++ b/lib/crypto/Cargo.toml @@ -1,9 +1,9 @@ [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.workspace = true diff --git a/lib/e2e-proc/Cargo.toml b/lib/e2e-proc/Cargo.toml index c89b39f3..aaa76ad1 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 0c48aa14..84a9c6df 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] diff --git a/lib/e2e/src/account.rs b/lib/e2e/src/account.rs index 5e0e9559..83d4cd26 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 a307472e..e65aef6e 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 0bd0d487..5d90cd8b 100644 --- a/lib/e2e/src/system.rs +++ b/lib/e2e/src/system.rs @@ -61,17 +61,19 @@ pub fn provider() -> Provider { } /// Send `amount` eth to `address` in the nitro-tesnode. -pub fn fund_account(address: Address, amount: &str) -> eyre::Result<()> { - // ./test-node.bash script send-l2 --to - // address_0x01fA6bf4Ee48B6C95900BCcf9BEA172EF5DBd478 --ethamount 10 +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`") + }; + let output = std::process::Command::new(node_script) .arg("script") .arg("send-l2") .arg("--to") .arg(format!("address_{address}")) .arg("--ethamount") - .arg(amount) + .arg(amount.to_string()) .output()?; if !output.status.success() { diff --git a/lib/motsu-proc/Cargo.toml b/lib/motsu-proc/Cargo.toml index 7b172b60..99cbdb6d 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" @@ -17,7 +18,6 @@ motsu.workspace = true alloy-primitives.workspace = true alloy-sol-types.workspace = true stylus-sdk.workspace = true -stylus-proc.workspace = true [lib] proc-macro = true diff --git a/lib/motsu/Cargo.toml b/lib/motsu/Cargo.toml index 57f1f19a..5f3e5958 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" diff --git a/scripts/bench.sh b/scripts/bench.sh index 8878daec..1801d6c3 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!" diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index 3a26eacb..f7b9cdeb 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -5,7 +5,7 @@ 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 diff --git a/scripts/nitro-testnode.sh b/scripts/nitro-testnode.sh index e14a8da4..4e807379 100755 --- a/scripts/nitro-testnode.sh +++ b/scripts/nitro-testnode.sh @@ -17,15 +17,22 @@ do HAS_DETACH=true shift ;; - -down|--shutdown) - docker container stop "$(docker container ls -q --filter name=nitro-testnode)" + -q|--quit) + 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 ;; *) echo "OPTIONS:" echo "-i|--init: clone repo and init nitro test node" echo "-d|--detach: setup nitro test node in detached mode" - echo "-down|--shutdown: shutdown nitro test node docker containers" + echo "-q|--quit: shutdown nitro test node docker containers" exit 0 ;; esac @@ -43,10 +50,10 @@ 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 --no-tokenbridge || exit + ./test-node.bash --no-run --init || exit fi