diff --git a/Cargo.lock b/Cargo.lock index 14203f897..904e20c2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,6 +127,7 @@ version = "0.1.0" dependencies = [ "alloy-primitives 0.3.3", "alloy-sol-types", + "cfg-if 1.0.0", "derive_more", "grip", "mini-alloc", @@ -225,6 +226,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "erc20-example" +version = "0.0.0" +dependencies = [ + "contracts", + "mini-alloc", + "stylus-proc", + "stylus-sdk", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -382,12 +393,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bitflags", - "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", - "regex-syntax", "unarray", ] diff --git a/Cargo.toml b/Cargo.toml index b276e3211..5c0d0c9b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,11 @@ [workspace] -members = ["contracts", "lib/crypto", "lib/grip", "lib/grip-proc"] +members = [ + "contracts", + "lib/crypto", + "lib/grip", + "lib/grip-proc", + "examples/erc20", +] # Explicitly set the resolver to version 2, which is the default for packages # with edition >= 2021. # https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html @@ -12,6 +18,13 @@ license = "MIT" keywords = ["arbitrum", "ethereum", "stylus"] repository = "https://github.com/OpenZeppelin/rust-contracts-stylus" +[workspace.dependencies] +alloy-primitives = { version = "0.3.1", default-features = false } +alloy-sol-types = { version = "0.3.1", default-features = false } +stylus-sdk = { version = "0.4.3", default-features = false } +stylus-proc = { version = "0.4.3", default-features = false } +mini-alloc = "0.4.2" + [profile.release] codegen-units = 1 panic = "abort" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index ca5437bce..fe01c1d9b 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -10,12 +10,13 @@ repository.workspace = true version = "0.1.0" [dependencies] -alloy-primitives = { version = "0.3.1", default-features = false } -alloy-sol-types = { version = "0.3.1", default-features = false } -stylus-sdk = { version = "0.4.3", default-features = false } -stylus-proc = { version = "0.4.3", default-features = false } -mini-alloc = "0.4.2" +alloy-primitives.workspace = true +alloy-sol-types.workspace = true +stylus-sdk.workspace = true +stylus-proc.workspace = true +mini-alloc.workspace = true derive_more = "0.99.17" +cfg-if = "1.0" [dev-dependencies] grip = { path = "../lib/grip" } @@ -24,31 +25,11 @@ once_cell = "1.19.0" [features] default = [] +tests = [] erc20 = [] erc20_metadata = ["erc20"] erc20_burnable = ["erc20"] erc721 = [] [lib] -# The Stylus team sets new crates with the following types: -# `lib` - The default, which gets turned to `rlib` by cargo and is needed to -# link the crate as a dependency of binaries. -# `cdylib` - A dynamic system library to be loaded from `wasm`. -# -# See -# -# This means our crate would be built twice: once as a rust library and once as -# a dynamic library. When running `cargo test`, cargo invokes rustc twice, but -# when it build the rust library, it doesn't set the `test` feature flag, so we -# can't use it. -# -# The reason to add `lib` is to be able to use the `export-abi` feature of the -# SDK. We don't set `lib` here because our contracts are meant to be used as an -# addition to other contracts. For this use case, the abi of those contracts -# will contain ours. -# -# The trade-off is being able to run `cargo test` with conditional compilation -# vs being able to run `cargo stylus export-abi`. -# -# This may change in the future, so this behavior should not be relied upon. -crate-type = ["cdylib"] +crate-type = ["lib", "cdylib"] diff --git a/contracts/src/erc20/extensions/metadata.rs b/contracts/src/erc20/extensions/metadata.rs index af9c0215f..6237a5e61 100644 --- a/contracts/src/erc20/extensions/metadata.rs +++ b/contracts/src/erc20/extensions/metadata.rs @@ -36,7 +36,7 @@ impl Metadata { /// * `name` - The name of the token. /// * `symbol` - The symbol of the token. pub fn constructor(&mut self, name: String, symbol: String) { - if self._initialized.get() == true { + if self._initialized.get() { return; } @@ -87,7 +87,7 @@ impl Metadata { } } -#[cfg(test)] +#[cfg(all(test, feature = "tests"))] mod tests { use alloy_primitives::U256; use stylus_sdk::storage::{StorageBool, StorageString, StorageType}; diff --git a/contracts/src/erc20/extensions/mod.rs b/contracts/src/erc20/extensions/mod.rs index 4eb875e72..1c1b32057 100644 --- a/contracts/src/erc20/extensions/mod.rs +++ b/contracts/src/erc20/extensions/mod.rs @@ -1,5 +1,13 @@ -#[cfg(any(test, erc20_metadata))] -pub mod metadata; +//! Common extensions to the ERC-20 standard. -#[cfg(any(test, erc20_burnable))] -pub mod burnable; +cfg_if::cfg_if! { + if #[cfg(any(test, feature = "erc20_metadata"))] { + pub mod metadata; + pub use metadata::Metadata; + } +} +cfg_if::cfg_if! { + if #[cfg(any(test, feature = "erc20_burnable"))] { + pub mod burnable; + } +} diff --git a/contracts/src/erc20/mod.rs b/contracts/src/erc20/mod.rs index 2e2400faf..1b80cf3a5 100644 --- a/contracts/src/erc20/mod.rs +++ b/contracts/src/erc20/mod.rs @@ -437,7 +437,7 @@ impl ERC20 { } } -#[cfg(test)] +#[cfg(all(test, feature = "tests"))] mod tests { use alloy_primitives::{address, Address, U256}; use stylus_sdk::{ diff --git a/contracts/src/erc721/mod.rs b/contracts/src/erc721/mod.rs index 8df26a002..f2de0de76 100644 --- a/contracts/src/erc721/mod.rs +++ b/contracts/src/erc721/mod.rs @@ -1,3 +1,6 @@ +//! Implementation of the ERC-721 token standard. +use alloc::vec; + use alloy_primitives::{fixed_bytes, Address, FixedBytes, U128, U256}; use derive_more::From; use stylus_sdk::{ @@ -88,13 +91,26 @@ sol! { /// [ERC-6093]: https://eips.ethereum.org/EIPS/eip-6093 #[derive(SolidityError, Debug, From)] pub enum Error { + /// Indicates that an address can't be an owner. + /// For example, `address(0)` is a forbidden owner in ERC-721. Used in + /// balance queries. InvalidOwner(ERC721InvalidOwner), + /// Indicates a `tokenId` whose `owner` is the zero address. NonexistentToken(ERC721NonexistentToken), + /// Indicates an error related to the ownership over a particular token. + /// Used in transfers. IncorrectOwner(ERC721IncorrectOwner), + /// Indicates a failure with the token `sender`. Used in transfers. InvalidSender(ERC721InvalidSender), + /// Indicates a failure with the token `receiver`. Used in transfers. InvalidReceiver(ERC721InvalidReceiver), + /// Indicates a failure with the `operator`’s approval. Used in transfers. InsufficientApproval(ERC721InsufficientApproval), + /// Indicates a failure with the `approver` of a token to be approved. Used + /// in approvals. InvalidApprover(ERC721InvalidApprover), + /// Indicates a failure with the `operator` to be approved. Used in + /// approvals. InvalidOperator(ERC721InvalidOperator), } @@ -120,13 +136,15 @@ sol_interface! { } sol_storage! { + /// State of an ERC-721 token. pub struct ERC721 { + /// Maps tokens to owners. mapping(uint256 => address) _owners; - + /// Maps users to balances. mapping(address => uint256) _balances; - + /// Maps tokens to approvals. mapping(uint256 => address) _token_approvals; - + /// Maps owners to a mapping of operator approvals. mapping(address => mapping(address => bool)) _operator_approvals; } } @@ -272,7 +290,7 @@ impl ERC721 { data: Bytes, ) -> Result<(), Error> { self.transfer_from(from, to, token_id)?; - self._check_on_erc721_received(msg::sender(), from, to, token_id, data) + self._check_on_erc721_received(msg::sender(), from, to, token_id, &data) } /// Transfers `token_id` token from `from` to `to`. @@ -706,7 +724,7 @@ impl ERC721 { Address::ZERO, to, token_id, - data, + &data, ) } @@ -847,7 +865,7 @@ impl ERC721 { data: Bytes, ) -> Result<(), Error> { self._transfer(from, to, token_id)?; - self._check_on_erc721_received(msg::sender(), from, to, token_id, data) + self._check_on_erc721_received(msg::sender(), from, to, token_id, &data) } /// Variant of `approve_inner` with an optional flag to enable or disable @@ -989,7 +1007,7 @@ impl ERC721 { from: Address, to: Address, token_id: U256, - data: Bytes, + data: &Bytes, ) -> Result<(), Error> { const IERC721RECEIVER_INTERFACE_ID: FixedBytes<4> = fixed_bytes!("150b7a02"); @@ -1017,7 +1035,7 @@ impl ERC721 { } } -#[cfg(test)] +#[cfg(all(test, feature = "tests"))] mod tests { use alloy_primitives::address; use once_cell::sync::Lazy; diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 8289fa33e..b38e9a2cd 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -1,19 +1,19 @@ #![doc = include_str!("../../README.md")] #![warn(missing_docs, unreachable_pub, rust_2021_compatibility)] #![warn(clippy::all, clippy::pedantic)] -#![cfg_attr(not(test), no_std, no_main)] +#![cfg_attr(not(feature = "tests"), no_std, no_main)] extern crate alloc; #[global_allocator] static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; mod arithmetic; -#[cfg(any(test, erc20))] +#[cfg(any(feature = "tests", feature = "erc20"))] pub mod erc20; -#[cfg(any(test, erc721))] +#[cfg(any(feature = "tests", feature = "erc721"))] pub mod erc721; -#[cfg(not(any(test, target_arch = "wasm32-unknown-unknown")))] +#[cfg(not(any(feature = "tests", target_arch = "wasm32-unknown-unknown")))] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} diff --git a/examples/erc20/Cargo.toml b/examples/erc20/Cargo.toml new file mode 100644 index 000000000..0bfccfe08 --- /dev/null +++ b/examples/erc20/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "erc20-example" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version = "0.0.0" + +[dependencies] +contracts = { path = "../../contracts", features = ["erc20", "erc20_metadata"] } +stylus-sdk.workspace = true +stylus-proc.workspace = true +mini-alloc.workspace = true + +[lib] +crate-type = ["lib", "cdylib"] diff --git a/examples/erc20/src/lib.rs b/examples/erc20/src/lib.rs new file mode 100644 index 000000000..4ecce2799 --- /dev/null +++ b/examples/erc20/src/lib.rs @@ -0,0 +1,35 @@ +#![cfg_attr(not(test), no_main, no_std)] +extern crate alloc; + +use alloc::string::String; + +use contracts::erc20::{extensions::Metadata, ERC20}; +use stylus_sdk::prelude::{entrypoint, external, sol_storage}; + +const DECIMALS: u8 = 10; + +sol_storage! { + #[entrypoint] + struct Token { + #[borrow] + ERC20 erc20; + #[borrow] + Metadata metadata; + } +} + +#[external] +#[inherit(ERC20, Metadata)] +impl Token { + pub fn constructor(&mut self, name: String, symbol: String) { + self.metadata.constructor(name, symbol); + } + + // Overrides the default [`Metadata::decimals`], and sets it to `10`. + // + // If you don't provide this method in the `entrypoint` contract, it will + // default to `18`. + pub fn decimals(&self) -> u8 { + DECIMALS + } +} diff --git a/lib/crypto/Cargo.toml b/lib/crypto/Cargo.toml index 62246bdcf..451a7126c 100644 --- a/lib/crypto/Cargo.toml +++ b/lib/crypto/Cargo.toml @@ -8,11 +8,9 @@ license.workspace = true repository.workspace = true version = "0.1.0" -[dependencies] -alloy-primitives = { version = "0.6.4", default-features = false } - [dev-dependencies] -const-hex = "1.11.1" +alloy-primitives = { version = "0.6.4", default-features = false } +const-hex = { version = "1.11.1", default-features = false } rand = "0.8.5" [features]