From c9e4ffa3c4f0b5f170d114f0744619f4a1a69a14 Mon Sep 17 00:00:00 2001 From: Arpit Temani Date: Mon, 4 Aug 2025 18:41:17 +0400 Subject: [PATCH 1/4] feat: erc721 holder example --- Cargo.lock | 14 ++++ Cargo.toml | 2 + examples/erc721-holder/Cargo.toml | 30 ++++++++ examples/erc721-holder/src/lib.rs | 34 +++++++++ examples/erc721-holder/src/main.rs | 10 +++ examples/erc721-holder/tests/abi/mod.rs | 16 ++++ examples/erc721-holder/tests/erc721-holder.rs | 75 +++++++++++++++++++ 7 files changed, 181 insertions(+) create mode 100644 examples/erc721-holder/Cargo.toml create mode 100644 examples/erc721-holder/src/lib.rs create mode 100644 examples/erc721-holder/src/main.rs create mode 100644 examples/erc721-holder/tests/abi/mod.rs create mode 100644 examples/erc721-holder/tests/erc721-holder.rs diff --git a/Cargo.lock b/Cargo.lock index 78c314ca3..35131f9e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1910,6 +1910,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "erc721-holder-example" +version = "0.3.0-alpha.1" +dependencies = [ + "alloy", + "alloy-primitives", + "alloy-sol-types", + "e2e", + "eyre", + "openzeppelin-stylus", + "stylus-sdk", + "tokio", +] + [[package]] name = "erc721-metadata-example" version = "0.3.0-alpha.1" diff --git a/Cargo.toml b/Cargo.toml index dff840dca..783c79d25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "examples/erc20-wrapper", "examples/erc721", "examples/erc721-consecutive", + "examples/erc721-holder", "examples/erc721-metadata", "examples/erc721-wrapper", "examples/erc1155", @@ -48,6 +49,7 @@ default-members = [ "examples/erc20-wrapper", "examples/erc721", "examples/erc721-consecutive", + "examples/erc721-holder", "examples/erc721-metadata", "examples/erc721-wrapper", "examples/erc1155", diff --git a/examples/erc721-holder/Cargo.toml b/examples/erc721-holder/Cargo.toml new file mode 100644 index 000000000..20ece14e9 --- /dev/null +++ b/examples/erc721-holder/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "erc721-holder-example" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[dependencies] +openzeppelin-stylus.workspace = true +alloy-primitives.workspace = true +alloy-sol-types.workspace = true +stylus-sdk.workspace = true + +[dev-dependencies] +alloy.workspace = true +e2e.workspace = true +tokio.workspace = true +eyre.workspace = true + +[lib] +crate-type = ["lib", "cdylib"] + +[features] +e2e = [] +export-abi = ["openzeppelin-stylus/export-abi"] + +[[bin]] +name = "erc721-holder-example" +path = "src/main.rs" diff --git a/examples/erc721-holder/src/lib.rs b/examples/erc721-holder/src/lib.rs new file mode 100644 index 000000000..fb7a94b2d --- /dev/null +++ b/examples/erc721-holder/src/lib.rs @@ -0,0 +1,34 @@ +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] +extern crate alloc; + +use openzeppelin_stylus::token::erc721::utils::Erc721Holder; +use stylus_sdk::prelude::*; + +#[entrypoint] +#[storage] +struct Erc721HolderExample { + holder: Erc721Holder, +} + +#[public] +impl Erc721HolderExample { + #[constructor] + pub fn constructor(&mut self) -> Result<(), Vec> { + Ok(()) + } +} + +// Implement the IErc721Receiver trait by delegating to the Erc721Holder +impl openzeppelin_stylus::token::erc721::receiver::IErc721Receiver for Erc721HolderExample { + type Error = Vec; + + fn on_erc721_received( + &mut self, + operator: alloy_primitives::Address, + from: alloy_primitives::Address, + token_id: alloy_primitives::U256, + data: stylus_sdk::abi::Bytes, + ) -> Result { + self.holder.on_erc721_received(operator, from, token_id, data) + } +} diff --git a/examples/erc721-holder/src/main.rs b/examples/erc721-holder/src/main.rs new file mode 100644 index 000000000..13107b82d --- /dev/null +++ b/examples/erc721-holder/src/main.rs @@ -0,0 +1,10 @@ +#![cfg_attr(not(any(test, feature = "export-abi")), no_main)] + +#[cfg(not(any(test, feature = "export-abi")))] +#[no_mangle] +pub extern "C" fn main() {} + +#[cfg(feature = "export-abi")] +fn main() { + erc721_holder_example::print_from_args(); +} diff --git a/examples/erc721-holder/tests/abi/mod.rs b/examples/erc721-holder/tests/abi/mod.rs new file mode 100644 index 000000000..ee96377cf --- /dev/null +++ b/examples/erc721-holder/tests/abi/mod.rs @@ -0,0 +1,16 @@ +#![cfg(feature = "e2e")] + +use alloy::sol; + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + contract Erc721HolderExample { + function onERC721Received( + address operator, + address from, + uint256 tokenId, + bytes calldata data + ) external returns (bytes4); + } +} diff --git a/examples/erc721-holder/tests/erc721-holder.rs b/examples/erc721-holder/tests/erc721-holder.rs new file mode 100644 index 000000000..a58379b96 --- /dev/null +++ b/examples/erc721-holder/tests/erc721-holder.rs @@ -0,0 +1,75 @@ +#![cfg(feature = "e2e")] + +use abi::Erc721HolderExample; +use alloy::primitives::U256; +use e2e::{constructor, Account}; +use eyre::Result; +use stylus_sdk::function_selector; + +mod abi; + +const RECEIVER_FN_SELECTOR: [u8; 4] = function_selector!( + "onERC721Received", + alloy_primitives::Address, + alloy_primitives::Address, + U256, + stylus_sdk::abi::Bytes, +); + +#[e2e::test] +async fn deploys_successfully(alice: Account) -> Result<()> { + let contract_addr = alice + .as_deployer() + .with_constructor(constructor!()) + .with_example_name("erc721-holder") + .deploy() + .await? + .contract_address; + + let contract = Erc721HolderExample::new(contract_addr, &alice.wallet); + + // Test that the contract can be called and returns the correct selector + let result = contract + .onERC721Received( + alice.address(), + alice.address(), + U256::from(1), + alloy_primitives::Bytes::new(), + ) + .call() + .await?; + + assert_eq!(result._0, RECEIVER_FN_SELECTOR); + + Ok(()) +} + +#[e2e::test] +async fn returns_correct_selector_with_data(alice: Account) -> Result<()> { + let contract_addr = alice + .as_deployer() + .with_constructor(constructor!()) + .with_example_name("erc721-holder") + .deploy() + .await? + .contract_address; + + let contract = Erc721HolderExample::new(contract_addr, &alice.wallet); + + // Test with some data + let test_data = alloy_primitives::Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]); + + let result = contract + .onERC721Received( + alice.address(), + alice.address(), + U256::from(42), + test_data, + ) + .call() + .await?; + + assert_eq!(result._0, RECEIVER_FN_SELECTOR); + + Ok(()) +} From a15c09756876f02b65c1d491bfb70c4ea0175d9b Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Tue, 5 Aug 2025 11:18:04 +0200 Subject: [PATCH 2/4] fix: E2E test --- examples/erc721-holder/src/lib.rs | 26 ++++++---- examples/erc721-holder/tests/abi/mod.rs | 8 ++- examples/erc721-holder/tests/erc721-holder.rs | 52 +++++-------------- 3 files changed, 34 insertions(+), 52 deletions(-) diff --git a/examples/erc721-holder/src/lib.rs b/examples/erc721-holder/src/lib.rs index fb7a94b2d..8f0f91bb3 100644 --- a/examples/erc721-holder/src/lib.rs +++ b/examples/erc721-holder/src/lib.rs @@ -1,8 +1,14 @@ #![cfg_attr(not(any(test, feature = "export-abi")), no_main)] extern crate alloc; -use openzeppelin_stylus::token::erc721::utils::Erc721Holder; -use stylus_sdk::prelude::*; +use openzeppelin_stylus::token::erc721::{ + receiver::IErc721Receiver, utils::Erc721Holder, +}; +use stylus_sdk::{ + abi::Bytes, + alloy_primitives::{aliases::B32, Address, U256}, + prelude::*, +}; #[entrypoint] #[storage] @@ -11,6 +17,7 @@ struct Erc721HolderExample { } #[public] +#[implements(IErc721Receiver>)] impl Erc721HolderExample { #[constructor] pub fn constructor(&mut self) -> Result<(), Vec> { @@ -18,17 +25,18 @@ impl Erc721HolderExample { } } -// Implement the IErc721Receiver trait by delegating to the Erc721Holder -impl openzeppelin_stylus::token::erc721::receiver::IErc721Receiver for Erc721HolderExample { +#[public] +impl IErc721Receiver for Erc721HolderExample { type Error = Vec; + #[selector(name = "onERC721Received")] fn on_erc721_received( &mut self, - operator: alloy_primitives::Address, - from: alloy_primitives::Address, - token_id: alloy_primitives::U256, - data: stylus_sdk::abi::Bytes, - ) -> Result { + operator: Address, + from: Address, + token_id: U256, + data: Bytes, + ) -> Result { self.holder.on_erc721_received(operator, from, token_id, data) } } diff --git a/examples/erc721-holder/tests/abi/mod.rs b/examples/erc721-holder/tests/abi/mod.rs index ee96377cf..46b31a1a3 100644 --- a/examples/erc721-holder/tests/abi/mod.rs +++ b/examples/erc721-holder/tests/abi/mod.rs @@ -1,9 +1,7 @@ -#![cfg(feature = "e2e")] - +#![allow(dead_code)] use alloy::sol; -sol! { - #[allow(missing_docs)] +sol!( #[sol(rpc)] contract Erc721HolderExample { function onERC721Received( @@ -13,4 +11,4 @@ sol! { bytes calldata data ) external returns (bytes4); } -} +); diff --git a/examples/erc721-holder/tests/erc721-holder.rs b/examples/erc721-holder/tests/erc721-holder.rs index a58379b96..824083fed 100644 --- a/examples/erc721-holder/tests/erc721-holder.rs +++ b/examples/erc721-holder/tests/erc721-holder.rs @@ -1,75 +1,51 @@ #![cfg(feature = "e2e")] use abi::Erc721HolderExample; -use alloy::primitives::U256; +use alloy::primitives::{Bytes, U256}; use e2e::{constructor, Account}; use eyre::Result; -use stylus_sdk::function_selector; +use openzeppelin_stylus::token::erc721::RECEIVER_FN_SELECTOR; mod abi; -const RECEIVER_FN_SELECTOR: [u8; 4] = function_selector!( - "onERC721Received", - alloy_primitives::Address, - alloy_primitives::Address, - U256, - stylus_sdk::abi::Bytes, -); - #[e2e::test] -async fn deploys_successfully(alice: Account) -> Result<()> { +async fn returns_correct_selector(alice: Account) -> Result<()> { let contract_addr = alice .as_deployer() .with_constructor(constructor!()) - .with_example_name("erc721-holder") .deploy() .await? .contract_address; let contract = Erc721HolderExample::new(contract_addr, &alice.wallet); - // Test that the contract can be called and returns the correct selector - let result = contract + // call without data. + let interface_selector = contract .onERC721Received( alice.address(), alice.address(), U256::from(1), - alloy_primitives::Bytes::new(), + Bytes::new(), ) .call() - .await?; - - assert_eq!(result._0, RECEIVER_FN_SELECTOR); - - Ok(()) -} - -#[e2e::test] -async fn returns_correct_selector_with_data(alice: Account) -> Result<()> { - let contract_addr = alice - .as_deployer() - .with_constructor(constructor!()) - .with_example_name("erc721-holder") - .deploy() .await? - .contract_address; + ._0; - let contract = Erc721HolderExample::new(contract_addr, &alice.wallet); - - // Test with some data - let test_data = alloy_primitives::Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]); + assert_eq!(RECEIVER_FN_SELECTOR, interface_selector); - let result = contract + // call with data. + let interface_selector = contract .onERC721Received( alice.address(), alice.address(), U256::from(42), - test_data, + Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]), ) .call() - .await?; + .await? + ._0; - assert_eq!(result._0, RECEIVER_FN_SELECTOR); + assert_eq!(RECEIVER_FN_SELECTOR, interface_selector); Ok(()) } From e2c1ffdee6a5711a69a6b096348176e08ec68f9a Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Tue, 5 Aug 2025 11:30:04 +0200 Subject: [PATCH 3/4] fix: docs issues --- contracts/src/token/erc721/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index ecd600425..e3c53be6c 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -517,8 +517,8 @@ impl Erc721 { /// not tracked by the core [`Erc721`] logic MUST be matched with the use /// of [`Self::_increase_balance`] to keep balances consistent with /// ownership. The invariant to preserve is that for any address `a` the - /// value returned by [`Self::balance_of(a)`] must be equal to the number of - /// tokens such that [`Self::_owner_of(token_id)`] is `a`. + /// value returned by `Self::balance_of(a)` must be equal to the number of + /// tokens such that `Self::_owner_of(token_id)` is `a`. /// /// # Arguments /// From da4379302d84ba947513d6328a4f67cefa6061f8 Mon Sep 17 00:00:00 2001 From: Daniel Bigos Date: Wed, 6 Aug 2025 18:08:01 +0200 Subject: [PATCH 4/4] fix: Cargo.toml --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 671a7f6f4..c8384c70f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ members = [ "examples/erc20-wrapper", "examples/erc721", "examples/erc721-consecutive", - "examples/erc721-holder", "examples/erc721-metadata", "examples/erc721-wrapper", "examples/erc1155", @@ -52,7 +51,6 @@ default-members = [ "examples/erc20-wrapper", "examples/erc721", "examples/erc721-consecutive", - "examples/erc721-holder", "examples/erc721-metadata", "examples/erc721-wrapper", "examples/erc1155",