From a19789aea4c31fe147fbf7a6d8740fd6c4349c5c Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 10 Dec 2024 11:50:45 +0100 Subject: [PATCH] feat: add ERC1155Supply extension (#418) Resolves #350 Built off of [erc1155-supply](https://github.com/OpenZeppelin/rust-contracts-stylus/tree/erc1155-supply). Due to major merge conflicts when trying to update the branch with `main`, decided to create a new branch and manually copy/paste the specific changes. For the additional updates to the original, check out the commit messages. #### PR Checklist - [x] Tests - [x] Documentation - [x] Changelog --------- Co-authored-by: Daniel Bigos Co-authored-by: Daniel Bigos --- CHANGELOG.md | 2 + Cargo.lock | 13 + Cargo.toml | 2 + benches/src/erc1155_supply.rs | 79 ++ benches/src/lib.rs | 1 + contracts/src/token/erc1155/extensions/mod.rs | 2 + .../src/token/erc1155/extensions/supply.rs | 521 ++++++++ contracts/src/token/erc1155/mod.rs | 2 +- contracts/src/utils/math/storage.rs | 18 + docs/modules/ROOT/pages/erc1155-supply.adoc | 46 + docs/modules/ROOT/pages/erc1155.adoc | 2 + examples/erc1155-supply/Cargo.toml | 24 + examples/erc1155-supply/src/lib.rs | 81 ++ examples/erc1155-supply/tests/abi/mod.rs | 36 + .../erc1155-supply/tests/erc1155-supply.rs | 1124 +++++++++++++++++ examples/erc1155-supply/tests/mock/mod.rs | 1 + .../erc1155-supply/tests/mock/receiver.rs | 113 ++ examples/erc1155/tests/erc1155.rs | 2 +- 18 files changed, 2067 insertions(+), 2 deletions(-) create mode 100644 benches/src/erc1155_supply.rs create mode 100644 contracts/src/token/erc1155/extensions/supply.rs create mode 100644 docs/modules/ROOT/pages/erc1155-supply.adoc create mode 100644 examples/erc1155-supply/Cargo.toml create mode 100644 examples/erc1155-supply/src/lib.rs create mode 100644 examples/erc1155-supply/tests/abi/mod.rs create mode 100644 examples/erc1155-supply/tests/erc1155-supply.rs create mode 100644 examples/erc1155-supply/tests/mock/mod.rs create mode 100644 examples/erc1155-supply/tests/mock/receiver.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bea83cf7..566060051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `Erc1155Supply` extension. #418 - `Erc1155Pausable`extension. #432 - `Erc1155UriStorage` extension. #431 - `VestingWallet` contract. #402 @@ -17,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Implement `AddAssignUnchecked` and `SubAssignUnchecked` for `StorageUint`. #418 - Implement `MethodError` for `safe_erc20::Error`. #402 - Use `function_selector!` to calculate transfer type selector in `Erc1155`. #417 - Update internal functions of `Erc721` and `Erc721Consecutive` accept reference to `Bytes`. #437 diff --git a/Cargo.lock b/Cargo.lock index 0e1934d5f..ff7be5413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1574,6 +1574,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "erc1155-supply-example" +version = "0.2.0-alpha.1" +dependencies = [ + "alloy", + "alloy-primitives", + "e2e", + "eyre", + "openzeppelin-stylus", + "stylus-sdk", + "tokio", +] + [[package]] name = "erc20-example" version = "0.2.0-alpha.1" diff --git a/Cargo.toml b/Cargo.toml index 61534133e..03bcca2fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "examples/erc721-metadata", "examples/erc1155", "examples/erc1155-metadata-uri", + "examples/erc1155-supply", "examples/merkle-proofs", "examples/ownable", "examples/vesting-wallet", @@ -39,6 +40,7 @@ default-members = [ "examples/erc721-metadata", "examples/erc1155", "examples/erc1155-metadata-uri", + "examples/erc1155-supply", "examples/safe-erc20", "examples/merkle-proofs", "examples/ownable", diff --git a/benches/src/erc1155_supply.rs b/benches/src/erc1155_supply.rs new file mode 100644 index 000000000..448e15272 --- /dev/null +++ b/benches/src/erc1155_supply.rs @@ -0,0 +1,79 @@ +use alloy::{ + network::{AnyNetwork, EthereumWallet}, + primitives::Address, + providers::ProviderBuilder, + sol, + sol_types::SolCall, + uint, +}; +use e2e::{receipt, Account}; + +use crate::{ + report::{ContractReport, FunctionReport}, + CacheOpt, +}; + +sol!( + #[sol(rpc)] + contract Erc1155Supply { + function mint(address to, uint256 id, uint256 amount, bytes memory data) external; + function totalSupply(uint256 id) external view returns (uint256); + function totalSupply() external view returns (uint256); + function exists(uint256 id) external view returns (bool); + } +); + +pub async fn bench() -> eyre::Result { + let reports = run_with(CacheOpt::None).await?; + let report = reports + .into_iter() + .try_fold(ContractReport::new("Erc1155Supply"), 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() + .network::() + .with_recommended_fillers() + .wallet(EthereumWallet::from(alice.signer.clone())) + .on_http(alice.url().parse()?); + + let contract_addr = deploy(&alice, cache_opt).await?; + + let contract = Erc1155Supply::new(contract_addr, &alice_wallet); + + let token = uint!(1_U256); + let value = uint!(100_U256); + + // IMPORTANT: Order matters! + use Erc1155Supply::*; + #[rustfmt::skip] + let receipts = vec![ + (mintCall::SIGNATURE, receipt!(contract.mint(alice_addr, token, value, vec![].into()))?), + (existsCall::SIGNATURE, receipt!(contract.exists(token))?), + (totalSupply_0Call::SIGNATURE, receipt!(contract.totalSupply_0(token))?), + (totalSupply_1Call::SIGNATURE, receipt!(contract.totalSupply_1())?), + ]; + + receipts + .into_iter() + .map(FunctionReport::new) + .collect::>>() +} + +async fn deploy( + account: &Account, + cache_opt: CacheOpt, +) -> eyre::Result
{ + crate::deploy(account, "erc1155-supply", None, cache_opt).await +} diff --git a/benches/src/lib.rs b/benches/src/lib.rs index 33e21373d..bdad0189c 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -16,6 +16,7 @@ use serde::Deserialize; pub mod access_control; pub mod erc1155; pub mod erc1155_metadata_uri; +pub mod erc1155_supply; pub mod erc20; pub mod erc721; pub mod merkle_proofs; diff --git a/contracts/src/token/erc1155/extensions/mod.rs b/contracts/src/token/erc1155/extensions/mod.rs index e2c5cafb1..0e046f3b4 100644 --- a/contracts/src/token/erc1155/extensions/mod.rs +++ b/contracts/src/token/erc1155/extensions/mod.rs @@ -1,8 +1,10 @@ //! Common extensions to the ERC-1155 standard. pub mod burnable; pub mod metadata_uri; +pub mod supply; pub mod uri_storage; pub use burnable::IErc1155Burnable; pub use metadata_uri::{Erc1155MetadataUri, IErc1155MetadataUri}; +pub use supply::{Erc1155Supply, IErc1155Supply}; pub use uri_storage::Erc1155UriStorage; diff --git a/contracts/src/token/erc1155/extensions/supply.rs b/contracts/src/token/erc1155/extensions/supply.rs new file mode 100644 index 000000000..0c5e218c3 --- /dev/null +++ b/contracts/src/token/erc1155/extensions/supply.rs @@ -0,0 +1,521 @@ +//! Extension of ERC-1155 that adds tracking of total supply per token id. +//! +//! Useful for scenarios where Fungible and Non-fungible tokens have to be +//! clearly identified. Note: While a `_total_supply` of 1 might mean the +//! corresponding is an NFT, there are no guarantees that no other tokens +//! with the same id are not going to be minted. +//! +//! NOTE: This contract implies a global limit of 2**256 - 1 to the number +//! of tokens that can be minted. +//! +//! CAUTION: This extension should not be added in an upgrade to an already +//! deployed contract. + +use alloc::{vec, vec::Vec}; + +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus_proc::interface_id; +use stylus_sdk::{ + abi::Bytes, + msg, + prelude::{public, sol_storage}, +}; + +use crate::{ + token::erc1155::{self, Erc1155, IErc1155}, + utils::math::storage::SubAssignUnchecked, +}; + +sol_storage! { + /// State of an [`Erc1155Supply`] token. + pub struct Erc1155Supply { + /// ERC-1155 contract storage. + Erc1155 erc1155; + /// Mapping from token id to total supply. + mapping(uint256 => uint256) _total_supply; + /// Total supply of all token ids. + uint256 _total_supply_all; + } +} + +/// Required interface of a [`Erc1155Supply`] contract. +#[interface_id] +pub trait IErc1155Supply { + /// Total value of tokens in with a given id. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `id` - Token id as a number. + fn total_supply(&self, id: U256) -> U256; + + /// Total value of tokens. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + #[selector(name = "totalSupply")] + fn total_supply_all(&self) -> U256; + + /// Indicates whether any token exist with a given id, or not. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `id` - Token id as a number. + fn exists(&self, id: U256) -> bool; +} + +impl IErc1155Supply for Erc1155Supply { + fn total_supply(&self, id: U256) -> U256 { + self._total_supply.get(id) + } + + fn total_supply_all(&self) -> U256 { + *self._total_supply_all + } + + fn exists(&self, id: U256) -> bool { + self.total_supply(id) > U256::ZERO + } +} + +#[public] +impl IErc1155 for Erc1155Supply { + type Error = erc1155::Error; + + fn balance_of(&self, account: Address, id: U256) -> U256 { + self.erc1155.balance_of(account, id) + } + + fn balance_of_batch( + &self, + accounts: Vec
, + ids: Vec, + ) -> Result, erc1155::Error> { + self.erc1155.balance_of_batch(accounts, ids) + } + + fn set_approval_for_all( + &mut self, + operator: Address, + approved: bool, + ) -> Result<(), erc1155::Error> { + self.erc1155.set_approval_for_all(operator, approved) + } + + fn is_approved_for_all(&self, account: Address, operator: Address) -> bool { + self.erc1155.is_approved_for_all(account, operator) + } + + fn safe_transfer_from( + &mut self, + from: Address, + to: Address, + id: U256, + value: U256, + data: Bytes, + ) -> Result<(), erc1155::Error> { + self.erc1155.authorize_transfer(from)?; + self.do_safe_transfer_from(from, to, vec![id], vec![value], &data) + } + + fn safe_batch_transfer_from( + &mut self, + from: Address, + to: Address, + ids: Vec, + values: Vec, + data: Bytes, + ) -> Result<(), erc1155::Error> { + self.erc1155.authorize_transfer(from)?; + self.do_safe_transfer_from(from, to, ids, values, &data) + } +} + +impl Erc1155Supply { + /// Creates a `value` amount of tokens of type `id`, and assigns + /// them to `to`. + /// + /// Re-export of [`Erc1155::_mint`]. + #[allow(clippy::missing_errors_doc)] + pub fn _mint( + &mut self, + to: Address, + id: U256, + value: U256, + data: &Bytes, + ) -> Result<(), erc1155::Error> { + self._do_mint(to, vec![id], vec![value], data) + } + + /// Batched version of [`Self::_mint`]. + /// + /// Re-export of [`Erc1155::_mint_batch`]. + #[allow(clippy::missing_errors_doc)] + pub fn _mint_batch( + &mut self, + to: Address, + ids: Vec, + values: Vec, + data: &Bytes, + ) -> Result<(), erc1155::Error> { + self._do_mint(to, ids, values, data) + } + + /// Destroys a `value` amount of tokens of type `id` from `from`. + /// + /// Re-export of [`Erc1155::_burn`]. + #[allow(clippy::missing_errors_doc)] + pub fn _burn( + &mut self, + from: Address, + id: U256, + value: U256, + ) -> Result<(), erc1155::Error> { + self._do_burn(from, vec![id], vec![value]) + } + + /// Batched version of [`Self::_burn`]. + /// + /// Re-export of [`Erc1155::_burn_batch`]. + #[allow(clippy::missing_errors_doc)] + pub fn _burn_batch( + &mut self, + from: Address, + ids: Vec, + values: Vec, + ) -> Result<(), erc1155::Error> { + self._do_burn(from, ids, values) + } +} + +impl Erc1155Supply { + /// Extended version of [`Erc1155::_update`] that updates the supply of + /// tokens. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account of the sender. + /// * `to` - Account of the recipient. + /// * `token_ids` - Array of all token id. + /// * `values` - Array of all amount of tokens to be supplied. + /// + /// # Errors + /// + /// If length of `ids` is not equal to length of `values`, then the + /// error [`erc1155::Error::InvalidArrayLength`] is returned. + /// If `value` is greater than the balance of the `from` account, + /// then the error [`erc1155::Error::InsufficientBalance`] is returned. + /// + /// NOTE: The ERC-1155 acceptance check is not performed in this function. + /// See [`Self::_update_with_acceptance_check`] instead. + /// + /// # Events + /// + /// Emits a [`erc1155::TransferSingle`] event if the arrays contain one + /// element, and [`erc1155::TransferBatch`] otherwise. + /// + /// # Panics + /// + /// If updated balance and/or supply exceeds `U256::MAX`, may happen during + /// the `mint` operation. + fn _update( + &mut self, + from: Address, + to: Address, + token_ids: Vec, + values: Vec, + ) -> Result<(), erc1155::Error> { + self.erc1155._update(from, to, token_ids.clone(), values.clone())?; + + if from.is_zero() { + for (&token_id, &value) in token_ids.iter().zip(values.iter()) { + let total_supply = + self.total_supply(token_id).checked_add(value).expect( + "should not exceed `U256::MAX` for `_total_supply`", + ); + self._total_supply.setter(token_id).set(total_supply); + } + + let total_mint_value = values.iter().sum(); + let total_supply_all = + self.total_supply_all().checked_add(total_mint_value).expect( + "should not exceed `U256::MAX` for `_total_supply_all`", + ); + self._total_supply_all.set(total_supply_all); + } + + if to.is_zero() { + for (token_id, &value) in token_ids.into_iter().zip(values.iter()) { + /* + * SAFETY: Overflow not possible: + * values[i] <= balance_of(from, token_ids[i]) <= + * total_supply(token_ids[i]) + */ + self._total_supply.setter(token_id).sub_assign_unchecked(value); + } + + let total_burn_value: U256 = values.into_iter().sum(); + /* + * SAFETY: Overflow not possible: + * total_burn_value = sum_i(values[i]) <= + * sum_i(total_supply(ids[i])) <= total_supply_all + */ + self._total_supply_all.sub_assign_unchecked(total_burn_value); + } + Ok(()) + } + + fn _update_with_acceptance_check( + &mut self, + from: Address, + to: Address, + ids: Vec, + values: Vec, + data: &Bytes, + ) -> Result<(), erc1155::Error> { + self._update(from, to, ids.clone(), values.clone())?; + + if !to.is_zero() { + self.erc1155._check_on_erc1155_received( + msg::sender(), + from, + to, + erc1155::Erc1155ReceiverData::new(ids, values), + data.to_vec().into(), + )?; + } + + Ok(()) + } + + fn _do_mint( + &mut self, + to: Address, + ids: Vec, + values: Vec, + data: &Bytes, + ) -> Result<(), erc1155::Error> { + if to.is_zero() { + return Err(erc1155::Error::InvalidReceiver( + erc1155::ERC1155InvalidReceiver { receiver: to }, + )); + } + self._update_with_acceptance_check( + Address::ZERO, + to, + ids, + values, + data, + )?; + Ok(()) + } + + fn _do_burn( + &mut self, + from: Address, + ids: Vec, + values: Vec, + ) -> Result<(), erc1155::Error> { + if from.is_zero() { + return Err(erc1155::Error::InvalidSender( + erc1155::ERC1155InvalidSender { sender: from }, + )); + } + self._update_with_acceptance_check( + from, + Address::ZERO, + ids, + values, + &vec![].into(), + )?; + Ok(()) + } + + fn do_safe_transfer_from( + &mut self, + from: Address, + to: Address, + ids: Vec, + values: Vec, + data: &Bytes, + ) -> Result<(), erc1155::Error> { + if to.is_zero() { + return Err(erc1155::Error::InvalidReceiver( + erc1155::ERC1155InvalidReceiver { receiver: to }, + )); + } + if from.is_zero() { + return Err(erc1155::Error::InvalidSender( + erc1155::ERC1155InvalidSender { sender: from }, + )); + } + self._update_with_acceptance_check(from, to, ids, values, data) + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::{address, Address, U256}; + + use super::{Erc1155Supply, IErc1155Supply}; + use crate::token::erc1155::{ + tests::{random_token_ids, random_values}, + ERC1155InvalidReceiver, ERC1155InvalidSender, Error, IErc1155, + }; + + const ALICE: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); + const BOB: Address = address!("B0B0cB49ec2e96DF5F5fFB081acaE66A2cBBc2e2"); + + fn init( + contract: &mut Erc1155Supply, + receiver: Address, + size: usize, + ) -> (Vec, Vec) { + let token_ids = random_token_ids(size); + let values = random_values(size); + + contract + ._mint_batch( + receiver, + token_ids.clone(), + values.clone(), + &vec![].into(), + ) + .expect("should mint"); + (token_ids, values) + } + + #[motsu::test] + fn before_mint(contract: Erc1155Supply) { + let token_id = random_token_ids(1)[0]; + assert_eq!(U256::ZERO, contract.total_supply(token_id)); + assert_eq!(U256::ZERO, contract.total_supply_all()); + assert!(!contract.exists(token_id)); + } + + #[motsu::test] + fn after_mint_single(contract: Erc1155Supply) { + let (token_ids, values) = init(contract, ALICE, 1); + assert_eq!(values[0], contract.balance_of(ALICE, token_ids[0])); + assert_eq!(values[0], contract.total_supply(token_ids[0])); + assert_eq!(values[0], contract.total_supply_all()); + assert!(contract.exists(token_ids[0])); + } + + #[motsu::test] + fn after_mint_batch(contract: Erc1155Supply) { + let (token_ids, values) = init(contract, ALICE, 4); + for (&token_id, &value) in token_ids.iter().zip(values.iter()) { + assert_eq!(value, contract.balance_of(ALICE, token_id)); + assert_eq!(value, contract.total_supply(token_id)); + assert!(contract.exists(token_id)); + } + let total_supply_all: U256 = values.iter().sum(); + assert_eq!(total_supply_all, contract.total_supply_all()); + } + + #[motsu::test] + fn mint_reverts_on_invalid_receiver(contract: Erc1155Supply) { + let token_id = random_token_ids(1)[0]; + let two = U256::from(2); + let invalid_receiver = Address::ZERO; + + let err = contract + ._mint(invalid_receiver, token_id, two, &vec![].into()) + .expect_err("should revert with `InvalidReceiver`"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { + receiver + }) if receiver == invalid_receiver + )); + } + + #[motsu::test] + #[should_panic = "should not exceed `U256::MAX` for `_total_supply`"] + fn mint_panics_on_total_supply_overflow(contract: Erc1155Supply) { + let token_id = random_token_ids(1)[0]; + let two = U256::from(2); + let three = U256::from(3); + contract + ._mint(ALICE, token_id, U256::MAX / two, &vec![].into()) + .expect("should mint to ALICE"); + contract + ._mint(BOB, token_id, U256::MAX / two, &vec![].into()) + .expect("should mint to BOB"); + let _ = contract._mint(ALICE, token_id, three, &vec![].into()); + } + + #[motsu::test] + #[should_panic = "should not exceed `U256::MAX` for `_total_supply_all`"] + fn mint_panics_on_total_supply_all_overflow(contract: Erc1155Supply) { + let token_ids = random_token_ids(2); + contract + ._mint(ALICE, token_ids[0], U256::MAX, &vec![].into()) + .expect("should mint"); + let _ = + contract._mint(ALICE, token_ids[1], U256::from(1), &vec![].into()); + } + + #[motsu::test] + fn after_burn_single(contract: Erc1155Supply) { + let (token_ids, values) = init(contract, ALICE, 1); + contract._burn(ALICE, token_ids[0], values[0]).expect("should burn"); + + assert_eq!(U256::ZERO, contract.total_supply(token_ids[0])); + assert_eq!(U256::ZERO, contract.total_supply_all()); + assert!(!contract.exists(token_ids[0])); + } + + #[motsu::test] + fn after_burn_batch(contract: Erc1155Supply) { + let (token_ids, values) = init(contract, ALICE, 4); + contract + ._burn_batch(ALICE, token_ids.clone(), values.clone()) + .expect("should burn batch"); + + for &token_id in &token_ids { + assert_eq!( + U256::ZERO, + contract.erc1155.balance_of(ALICE, token_id) + ); + assert!(!contract.exists(token_id)); + assert_eq!(U256::ZERO, contract.total_supply(token_id)); + } + assert_eq!(U256::ZERO, contract.total_supply_all()); + } + + #[motsu::test] + fn burn_reverts_when_invalid_sender(contract: Erc1155Supply) { + let (token_ids, values) = init(contract, ALICE, 1); + let invalid_sender = Address::ZERO; + + let err = contract + ._burn(invalid_sender, token_ids[0], values[0]) + .expect_err("should not burn token for invalid sender"); + + assert!(matches!( + err, + Error::InvalidSender(ERC1155InvalidSender { + sender + }) if sender == invalid_sender + )); + } + + #[motsu::test] + fn supply_unaffected_by_no_op(contract: Erc1155Supply) { + let token_ids = random_token_ids(1); + let values = random_values(1); + + contract + ._update(Address::ZERO, Address::ZERO, token_ids.clone(), values) + .expect("should supply"); + assert_eq!(U256::ZERO, contract.total_supply(token_ids[0])); + assert_eq!(U256::ZERO, contract.total_supply_all()); + assert!(!contract.exists(token_ids[0])); + } +} diff --git a/contracts/src/token/erc1155/mod.rs b/contracts/src/token/erc1155/mod.rs index 34b28433a..521e92655 100644 --- a/contracts/src/token/erc1155/mod.rs +++ b/contracts/src/token/erc1155/mod.rs @@ -849,7 +849,7 @@ impl Erc1155 { /// # Errors /// /// If `to` is `Address::ZERO`, then the error - /// [`Error:InvalidReceiver`] is returned. + /// [`Error::InvalidReceiver`] is returned. /// If length of `ids` is not equal to length of `values`, then the /// error [`Error::InvalidArrayLength`] is returned. /// If [`IERC1155Receiver::on_erc_1155_received`] hasn't returned its diff --git a/contracts/src/utils/math/storage.rs b/contracts/src/utils/math/storage.rs index 50058f266..fb1e295ef 100644 --- a/contracts/src/utils/math/storage.rs +++ b/contracts/src/utils/math/storage.rs @@ -15,6 +15,15 @@ impl AddAssignUnchecked> } } +impl AddAssignUnchecked> + for StorageUint +{ + fn add_assign_unchecked(&mut self, rhs: Uint) { + let new_balance = self.get() + rhs; + self.set(new_balance); + } +} + pub(crate) trait SubAssignUnchecked { fn sub_assign_unchecked(&mut self, rhs: T); } @@ -27,3 +36,12 @@ impl SubAssignUnchecked> self.set(new_balance); } } + +impl SubAssignUnchecked> + for StorageUint +{ + fn sub_assign_unchecked(&mut self, rhs: Uint) { + let new_balance = self.get() - rhs; + self.set(new_balance); + } +} diff --git a/docs/modules/ROOT/pages/erc1155-supply.adoc b/docs/modules/ROOT/pages/erc1155-supply.adoc new file mode 100644 index 000000000..ea9ba4405 --- /dev/null +++ b/docs/modules/ROOT/pages/erc1155-supply.adoc @@ -0,0 +1,46 @@ += ERC-1155 Supply + +The OpenZeppelin xref:erc1155.adoc[ERC-1155] Supply extension that adds tracking of total supply per token id. +Useful for scenarios where Fungible and Non-fungible tokens have to be clearly identified. + +[[usage]] +== Usage + +In order to make an xref:erc1155.adoc[ERC-1155] token with https://docs.rs/openzeppelin-stylus/0.2.0-alpha.2/openzeppelin_stylus/token/erc1155/extensions/supply/index.html[Supply] flavour, +you need to reexport all the supply-related functions. +Make sure to apply the `#[selector(name = "totalSupply")]` attribute to the `total_supply_all` function! +You need to create the specified contract as follows: + +[source,rust] +---- +use openzeppelin_stylus::token::erc1155::extensions::{ + Erc1155Supply, IErc1155Supply, +}; + +sol_storage! { + #[entrypoint] + struct Erc1155Example { + #[borrow] + Erc1155Supply erc1155_supply; + } +} + +#[public] +#[inherit(Erc1155Supply)] +impl Erc1155Example { + fn total_supply(&self, id: U256) -> U256 { + self.erc1155_supply.total_supply(id) + } + + #[selector(name = "totalSupply")] + fn total_supply_all(&self) -> U256 { + self.erc1155_supply.total_supply_all() + } + + fn exists(&self, id: U256) -> bool { + self.erc1155_supply.exists(id) + } + + // ... +} +---- diff --git a/docs/modules/ROOT/pages/erc1155.adoc b/docs/modules/ROOT/pages/erc1155.adoc index 2fb611345..98b1cdbc9 100644 --- a/docs/modules/ROOT/pages/erc1155.adoc +++ b/docs/modules/ROOT/pages/erc1155.adoc @@ -14,3 +14,5 @@ Additionally, there are multiple custom extensions, including: * xref:erc1155-pausable.adoc[ERC-1155 Pausable]: A primitive to pause contract operation like token transfers, minting and burning. * xref:erc1155-uri-storage.adoc[ERC-1155 URI Storage]: A more flexible but more expensive way of storing URI metadata. + +* xref:erc1155-supply.adoc[ERC-1155 Supply]: Extension of the ERC-1155 standard that adds tracking of total supply per token id. diff --git a/examples/erc1155-supply/Cargo.toml b/examples/erc1155-supply/Cargo.toml new file mode 100644 index 000000000..f35a31b34 --- /dev/null +++ b/examples/erc1155-supply/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "erc1155-supply-example" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[dependencies] +openzeppelin-stylus.workspace = true +alloy-primitives = { workspace = true, features = ["tiny-keccak"] } +stylus-sdk.workspace = true + +[dev-dependencies] +alloy.workspace = true +eyre.workspace = true +tokio.workspace = true +e2e.workspace = true + +[features] +e2e = [] + +[lib] +crate-type = ["lib", "cdylib"] diff --git a/examples/erc1155-supply/src/lib.rs b/examples/erc1155-supply/src/lib.rs new file mode 100644 index 000000000..dd18816ed --- /dev/null +++ b/examples/erc1155-supply/src/lib.rs @@ -0,0 +1,81 @@ +#![cfg_attr(not(test), no_main)] +extern crate alloc; + +use alloc::vec::Vec; + +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::token::erc1155::extensions::{ + Erc1155Supply, IErc1155Supply, +}; +use stylus_sdk::{ + abi::Bytes, + prelude::{entrypoint, public, sol_storage}, +}; + +sol_storage! { + #[entrypoint] + struct Erc1155Example { + #[borrow] + Erc1155Supply erc1155_supply; + } +} +#[public] +#[inherit(Erc1155Supply)] +impl Erc1155Example { + fn total_supply(&self, id: U256) -> U256 { + self.erc1155_supply.total_supply(id) + } + + #[selector(name = "totalSupply")] + fn total_supply_all(&self) -> U256 { + self.erc1155_supply.total_supply_all() + } + + fn exists(&self, id: U256) -> bool { + self.erc1155_supply.exists(id) + } + + // Add token minting feature. + pub fn mint( + &mut self, + to: Address, + id: U256, + value: U256, + data: Bytes, + ) -> Result<(), Vec> { + self.erc1155_supply._mint(to, id, value, &data)?; + Ok(()) + } + + pub fn mint_batch( + &mut self, + to: Address, + ids: Vec, + values: Vec, + data: Bytes, + ) -> Result<(), Vec> { + self.erc1155_supply._mint_batch(to, ids, values, &data)?; + Ok(()) + } + + // Add token burning feature. + pub fn burn( + &mut self, + from: Address, + id: U256, + value: U256, + ) -> Result<(), Vec> { + self.erc1155_supply._burn(from, id, value)?; + Ok(()) + } + + pub fn burn_batch( + &mut self, + from: Address, + ids: Vec, + values: Vec, + ) -> Result<(), Vec> { + self.erc1155_supply._burn_batch(from, ids, values)?; + Ok(()) + } +} diff --git a/examples/erc1155-supply/tests/abi/mod.rs b/examples/erc1155-supply/tests/abi/mod.rs new file mode 100644 index 000000000..ee967b7c4 --- /dev/null +++ b/examples/erc1155-supply/tests/abi/mod.rs @@ -0,0 +1,36 @@ +#![allow(dead_code)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract Erc1155Supply { + function balanceOf(address account, uint256 id) external view returns (uint256 balance); + #[derive(Debug)] + function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[] memory balances); + function isApprovedForAll(address account, address operator) external view returns (bool approved); + function setApprovalForAll(address operator, bool approved) external; + function safeTransferFrom(address from, address to, uint256 id, uint256 value, bytes memory data) external; + function safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data) external; + function mint(address to, uint256 id, uint256 amount, bytes memory data) external; + function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) external; + function burn(address account, uint256 id, uint256 value) external; + function burnBatch(address account, uint256[] memory ids, uint256[] memory values) external; + function totalSupply(uint256 id) external view returns (uint256); + function totalSupply() external view returns (uint256); + function exists(uint256 id) external view returns (bool); + + error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); + error ERC1155InvalidOperator(address operator); + error ERC1155InvalidSender(address sender); + error ERC1155InvalidReceiver(address receiver); + error ERC1155MissingApprovalForAll(address operator, address owner); + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + + #[derive(Debug, PartialEq)] + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + #[derive(Debug, PartialEq)] + event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values); + #[derive(Debug, PartialEq)] + event ApprovalForAll(address indexed account, address indexed operator, bool approved); + } +); diff --git a/examples/erc1155-supply/tests/erc1155-supply.rs b/examples/erc1155-supply/tests/erc1155-supply.rs new file mode 100644 index 000000000..bf6682185 --- /dev/null +++ b/examples/erc1155-supply/tests/erc1155-supply.rs @@ -0,0 +1,1124 @@ +#![cfg(feature = "e2e")] + +use abi::Erc1155Supply; +use alloy::primitives::{Address, U256}; +use e2e::{ + receipt, send, watch, Account, EventExt, Panic, PanicCode, ReceiptExt, +}; +use mock::{receiver, receiver::ERC1155ReceiverMock}; + +mod abi; +mod mock; + +fn random_token_ids(size: usize) -> Vec { + (0..size).map(U256::from).collect() +} + +fn random_values(size: usize) -> Vec { + (1..=size).map(U256::from).collect() +} + +// ============================================================================ +// Integration Tests: ERC-1155 Supply Extension +// ============================================================================ + +#[e2e::test] +async fn constructs(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let token_id = random_token_ids(1)[0]; + + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + let total_supply_all = contract.totalSupply_1().call().await?._0; + let token_exists = contract.exists(token_id).call().await?._0; + + assert_eq!(U256::ZERO, total_supply); + assert_eq!(U256::ZERO, total_supply_all); + assert!(!token_exists); + + Ok(()) +} + +#[e2e::test] +async fn mint(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let receipt = + receipt!(contract.mint(alice_addr, token_id, value, vec![].into()))?; + + assert!(receipt.emits(Erc1155Supply::TransferSingle { + operator: alice_addr, + from: Address::ZERO, + to: alice_addr, + id: token_id, + value, + })); + + let balance = + contract.balanceOf(alice_addr, token_id).call().await?.balance; + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + let total_supply_all = contract.totalSupply_1().call().await?._0; + let token_exists = contract.exists(token_id).call().await?._0; + + assert_eq!(value, balance); + assert_eq!(value, total_supply); + assert_eq!(value, total_supply_all); + assert!(token_exists); + + Ok(()) +} + +#[e2e::test] +async fn mint_to_receiver_contract(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let receiver_addr = + receiver::deploy(&alice.wallet, ERC1155ReceiverMock::RevertType::None) + .await?; + + let alice_addr = alice.address(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let initial_receiver_balance = + contract.balanceOf(receiver_addr, token_id).call().await?.balance; + + let receipt = + receipt!(contract.mint(receiver_addr, token_id, value, vec![].into()))?; + + assert!(receipt.emits(Erc1155Supply::TransferSingle { + operator: alice_addr, + from: Address::ZERO, + to: receiver_addr, + id: token_id, + value + })); + + assert!(receipt.emits(ERC1155ReceiverMock::Received { + operator: alice_addr, + from: Address::ZERO, + id: token_id, + value, + data: vec![].into(), + })); + + let receiver_balance = + contract.balanceOf(receiver_addr, token_id).call().await?.balance; + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + let total_supply_all = contract.totalSupply_1().call().await?._0; + let token_exists = contract.exists(token_id).call().await?._0; + + assert_eq!(initial_receiver_balance + value, receiver_balance); + assert_eq!(value, total_supply); + assert_eq!(value, total_supply_all); + assert!(token_exists); + + Ok(()) +} + +#[e2e::test] +async fn mint_batch(alice: Account, bob: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_ids = random_token_ids(2); + let values = random_values(2); + + let accounts = vec![alice_addr, bob_addr]; + + for &account in &accounts { + let receipt = receipt!(contract.mintBatch( + account, + token_ids.clone(), + values.clone(), + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferBatch { + operator: alice_addr, + from: Address::ZERO, + to: account, + ids: token_ids.clone(), + values: values.clone() + })); + + let balances = contract + .balanceOfBatch(vec![account, account], token_ids.clone()) + .call() + .await? + .balances; + + assert_eq!(values, balances); + } + + let accounts_len = U256::from(accounts.len()); + + for (&token_id, &value) in token_ids.iter().zip(values.iter()) { + let token_exists = contract.exists(token_id).call().await?._0; + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + + assert_eq!(value * accounts_len, total_supply); + assert!(token_exists); + } + + let total_supply_all = contract.totalSupply_1().call().await?._0; + assert_eq!(values.iter().sum::() * accounts_len, total_supply_all); + + Ok(()) +} + +#[e2e::test] +async fn mint_batch_transfer_to_receiver_contract( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let receiver_addr = + receiver::deploy(&alice.wallet, ERC1155ReceiverMock::RevertType::None) + .await?; + + let alice_addr = alice.address(); + let token_ids = random_token_ids(2); + let values = random_values(2); + + let initial_receiver_balances = contract + .balanceOfBatch(vec![receiver_addr, receiver_addr], token_ids.clone()) + .call() + .await? + .balances; + + let receipt = receipt!(contract.mintBatch( + receiver_addr, + token_ids.clone(), + values.clone(), + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferBatch { + operator: alice_addr, + from: Address::ZERO, + to: receiver_addr, + ids: token_ids.clone(), + values: values.clone() + })); + + assert!(receipt.emits(ERC1155ReceiverMock::BatchReceived { + operator: alice_addr, + from: Address::ZERO, + ids: token_ids.clone(), + values: values.clone(), + data: vec![].into(), + })); + + let receiver_balances = contract + .balanceOfBatch(vec![receiver_addr, receiver_addr], token_ids.clone()) + .call() + .await? + .balances; + + for (idx, (&token_id, &value)) in + token_ids.iter().zip(values.iter()).enumerate() + { + let token_exists = contract.exists(token_id).call().await?._0; + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + + assert_eq!( + initial_receiver_balances[idx] + value, + receiver_balances[idx] + ); + assert_eq!(value, total_supply); + assert!(token_exists); + } + + let total_supply_all = contract.totalSupply_1().call().await?._0; + assert_eq!(values.iter().sum::(), total_supply_all); + + Ok(()) +} + +#[e2e::test] +async fn mint_panics_on_total_supply_overflow( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_ids(1)[0]; + let two = U256::from(2); + let three = U256::from(3); + + let _ = watch!(contract.mint( + alice_addr, + token_id, + U256::MAX / two, + vec![].into() + )); + let _ = watch!(contract.mint( + bob_addr, + token_id, + U256::MAX / two, + vec![].into() + )); + + let err = send!(contract.mint(alice_addr, token_id, three, vec![].into())) + .expect_err("should panic due to total_supply overflow"); + + assert!(err.panicked_with(PanicCode::ArithmeticOverflow)); + + Ok(()) +} + +#[e2e::test] +async fn mint_panics_on_total_supply_all_overflow( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let token_ids = random_token_ids(2); + + let _ = watch!(contract.mint( + alice_addr, + token_ids[0], + U256::MAX, + vec![].into() + )); + + let err = send!(contract.mint( + alice_addr, + token_ids[1], + U256::from(1), + vec![].into() + )) + .expect_err("should panic due to total_supply_all overflow"); + + assert!(err.panicked_with(PanicCode::ArithmeticOverflow)); + + Ok(()) +} + +#[e2e::test] +async fn burn(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let _ = watch!(contract.mint(alice_addr, token_id, value, vec![].into())); + + let receipt = receipt!(contract.burn(alice_addr, token_id, value))?; + + assert!(receipt.emits(Erc1155Supply::TransferSingle { + operator: alice_addr, + from: alice_addr, + to: Address::ZERO, + id: token_id, + value, + })); + + let token_exists = contract.exists(token_id).call().await?._0; + let balance = + contract.balanceOf(alice_addr, token_id).call().await?.balance; + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + let total_supply_all = contract.totalSupply_1().call().await?._0; + + assert_eq!(U256::ZERO, balance); + assert_eq!(U256::ZERO, total_supply); + assert_eq!(U256::ZERO, total_supply_all); + assert!(!token_exists); + + Ok(()) +} + +#[e2e::test] +async fn burn_with_approval(alice: Account, bob: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + let contract_bob = Erc1155Supply::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let _ = watch!(contract.mint(bob_addr, token_id, value, vec![].into())); + + let _ = watch!(contract_bob.setApprovalForAll(alice_addr, true)); + + let receipt = receipt!(contract.burn(bob_addr, token_id, value))?; + + assert!(receipt.emits(Erc1155Supply::TransferSingle { + operator: alice_addr, + from: bob_addr, + to: Address::ZERO, + id: token_id, + value, + })); + + let token_exists = contract.exists(token_id).call().await?._0; + let balance = contract.balanceOf(bob_addr, token_id).call().await?.balance; + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + let total_supply_all = contract.totalSupply_1().call().await?._0; + + assert_eq!(U256::ZERO, balance); + assert_eq!(U256::ZERO, total_supply); + assert_eq!(U256::ZERO, total_supply_all); + assert!(!token_exists); + + Ok(()) +} + +#[e2e::test] +async fn burn_batch(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let token_ids = random_token_ids(4); + let values = random_values(4); + + let _ = watch!(contract.mintBatch( + alice_addr, + token_ids.clone(), + values.clone(), + vec![].into() + )); + + let receipt = receipt!(contract.burnBatch( + alice_addr, + token_ids.clone(), + values.clone() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferBatch { + operator: alice_addr, + from: alice_addr, + to: Address::ZERO, + ids: token_ids.clone(), + values, + })); + + for token_id in token_ids { + let balance = + contract.balanceOf(alice_addr, token_id).call().await?.balance; + let token_exists = contract.exists(token_id).call().await?._0; + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + + assert_eq!(U256::ZERO, balance); + assert_eq!(U256::ZERO, total_supply); + assert!(!token_exists); + } + + let total_supply_all = contract.totalSupply_1().call().await?._0; + assert_eq!(U256::ZERO, total_supply_all); + + Ok(()) +} + +#[e2e::test] +async fn burn_batch_with_approval( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + let contract_bob = Erc1155Supply::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_ids = random_token_ids(4); + let values = random_values(4); + + let _ = watch!(contract.mintBatch( + bob_addr, + token_ids.clone(), + values.clone(), + vec![].into() + )); + + let _ = watch!(contract_bob.setApprovalForAll(alice_addr, true)); + + let receipt = receipt!(contract.burnBatch( + bob_addr, + token_ids.clone(), + values.clone() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferBatch { + operator: alice_addr, + from: bob_addr, + to: Address::ZERO, + ids: token_ids.clone(), + values, + })); + + for token_id in token_ids { + let balance = + contract.balanceOf(bob_addr, token_id).call().await?.balance; + let token_exists = contract.exists(token_id).call().await?._0; + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + + assert_eq!(U256::ZERO, balance); + assert_eq!(U256::ZERO, total_supply); + assert!(!token_exists); + } + + let total_supply_all = contract.totalSupply_1().call().await?._0; + assert_eq!(U256::ZERO, total_supply_all); + + Ok(()) +} + +#[e2e::test] +async fn supply_unaffected_by_safe_transfer_from( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let _ = watch!(contract.mint(alice_addr, token_id, value, vec![].into())); + + // assert balances as expected after mint + let alice_balance = + contract.balanceOf(alice_addr, token_id).call().await?.balance; + let bob_balance = + contract.balanceOf(bob_addr, token_id).call().await?.balance; + + assert_eq!(value, alice_balance); + assert_eq!(U256::ZERO, bob_balance); + + // total supplies (all) logic has been checked in other tests, assume valid + let initial_total_supply = + contract.totalSupply_0(token_id).call().await?._0; + let initial_total_supply_all = contract.totalSupply_1().call().await?._0; + + let receipt = receipt!(contract.safeTransferFrom( + alice_addr, + bob_addr, + token_id, + value, + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferSingle { + operator: alice_addr, + from: alice_addr, + to: bob_addr, + id: token_id, + value, + })); + + // assert balances updated as expected + let alice_balance = + contract.balanceOf(alice_addr, token_id).call().await?.balance; + let bob_balance = + contract.balanceOf(bob_addr, token_id).call().await?.balance; + + assert_eq!(U256::ZERO, alice_balance); + assert_eq!(value, bob_balance); + + // assert supply-related data remains unchanged + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + let total_supply_all = contract.totalSupply_1().call().await?._0; + let token_exists = contract.exists(token_id).call().await?._0; + + assert_eq!(initial_total_supply, total_supply); + assert_eq!(initial_total_supply_all, total_supply_all); + assert!(token_exists); + + Ok(()) +} + +#[e2e::test] +async fn supply_unaffected_by_safe_transfer_from_batch( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_ids = random_token_ids(4); + let values = random_values(4); + + let _ = watch!(contract.mintBatch( + alice_addr, + token_ids.clone(), + values.clone(), + vec![].into() + )); + + // assert balances as expected after mint + for (&token_id, &value) in token_ids.iter().zip(values.iter()) { + let alice_balance = + contract.balanceOf(alice_addr, token_id).call().await?.balance; + let bob_balance = + contract.balanceOf(bob_addr, token_id).call().await?.balance; + + assert_eq!(value, alice_balance); + assert_eq!(U256::ZERO, bob_balance); + } + + // total supplies (all) logic has been checked in other tests, assume valid + let mut initial_total_supplies: Vec = vec![]; + for &token_id in &token_ids { + let supply = contract.totalSupply_0(token_id).call().await?._0; + initial_total_supplies.push(supply); + } + let initial_total_supply_all = contract.totalSupply_1().call().await?._0; + + let receipt = receipt!(contract.safeBatchTransferFrom( + alice_addr, + bob_addr, + token_ids.clone(), + values.clone(), + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferBatch { + operator: alice_addr, + from: alice_addr, + to: bob_addr, + ids: token_ids.clone(), + values: values.clone(), + })); + + // assert balances updated as expected + for (&token_id, &value) in token_ids.iter().zip(values.iter()) { + let alice_balance = + contract.balanceOf(alice_addr, token_id).call().await?.balance; + let bob_balance = + contract.balanceOf(bob_addr, token_id).call().await?.balance; + + assert_eq!(U256::ZERO, alice_balance); + assert_eq!(value, bob_balance); + } + + // assert supply-related data remains unchanged + for (&token_id, &initial_total_supply) in + token_ids.iter().zip(initial_total_supplies.iter()) + { + let total_supply = contract.totalSupply_0(token_id).call().await?._0; + let token_exists = contract.exists(token_id).call().await?._0; + + assert_eq!(initial_total_supply, total_supply); + assert!(token_exists); + } + + let total_supply_all = contract.totalSupply_1().call().await?._0; + assert_eq!(initial_total_supply_all, total_supply_all); + + Ok(()) +} + +// ===================================================================== +// Integration Tests: Happy Paths of Re-exported functions from ERC-1155 +// ===================================================================== + +#[e2e::test] +async fn balance_of_zero_balance(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + let token_ids = random_token_ids(1); + + let Erc1155Supply::balanceOfReturn { balance } = + contract.balanceOf(alice.address(), token_ids[0]).call().await?; + assert_eq!(U256::ZERO, balance); + + Ok(()) +} + +#[e2e::test] +async fn balance_of_batch_zero_balance( + alice: Account, + bob: Account, + dave: Account, + charlie: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + let accounts = + vec![alice.address(), bob.address(), dave.address(), charlie.address()]; + let token_ids = random_token_ids(4); + + let Erc1155Supply::balanceOfBatchReturn { balances } = + contract.balanceOfBatch(accounts, token_ids).call().await?; + assert_eq!(vec![U256::ZERO, U256::ZERO, U256::ZERO, U256::ZERO], balances); + + Ok(()) +} + +#[e2e::test] +async fn set_approval_for_all( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + + let approved_value = true; + let receipt = + receipt!(contract.setApprovalForAll(bob_addr, approved_value))?; + + assert!(receipt.emits(Erc1155Supply::ApprovalForAll { + account: alice_addr, + operator: bob_addr, + approved: approved_value, + })); + + let Erc1155Supply::isApprovedForAllReturn { approved } = + contract.isApprovedForAll(alice_addr, bob_addr).call().await?; + assert_eq!(approved_value, approved); + + let approved_value = false; + let receipt = + receipt!(contract.setApprovalForAll(bob_addr, approved_value))?; + + assert!(receipt.emits(Erc1155Supply::ApprovalForAll { + account: alice_addr, + operator: bob_addr, + approved: approved_value, + })); + + let Erc1155Supply::isApprovedForAllReturn { approved } = + contract.isApprovedForAll(alice_addr, bob_addr).call().await?; + assert_eq!(approved_value, approved); + + Ok(()) +} + +#[e2e::test] +async fn is_approved_for_all_zero_address(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let invalid_operator = Address::ZERO; + + let Erc1155Supply::isApprovedForAllReturn { approved } = contract + .isApprovedForAll(alice.address(), invalid_operator) + .call() + .await?; + + assert_eq!(false, approved); + + Ok(()) +} + +#[e2e::test] +async fn safe_transfer_from(alice: Account, bob: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + let _ = watch!(contract.mint( + alice_addr, + token_id, + value, + vec![0, 1, 2, 3].into() + )); + + let Erc1155Supply::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr, token_id).call().await?; + let Erc1155Supply::balanceOfReturn { balance: initial_bob_balance } = + contract.balanceOf(bob_addr, token_id).call().await?; + + let receipt = receipt!(contract.safeTransferFrom( + alice_addr, + bob_addr, + token_id, + value, + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferSingle { + operator: alice_addr, + from: alice_addr, + to: bob_addr, + id: token_id, + value + })); + + let Erc1155Supply::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr, token_id).call().await?; + assert_eq!(initial_alice_balance - value, alice_balance); + + let Erc1155Supply::balanceOfReturn { balance: bob_balance } = + contract.balanceOf(bob_addr, token_id).call().await?; + assert_eq!(initial_bob_balance + value, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn safe_transfer_from_with_approval( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract_alice = Erc1155Supply::new(contract_addr, &alice.wallet); + let contract_bob = Erc1155Supply::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let _ = watch!(contract_bob.mint( + bob_addr, + token_id, + value, + vec![0, 1, 2, 3].into() + )); + + let _ = watch!(contract_bob.setApprovalForAll(alice_addr, true)); + + let Erc1155Supply::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr, token_id).call().await?; + let Erc1155Supply::balanceOfReturn { balance: initial_bob_balance } = + contract_alice.balanceOf(bob_addr, token_id).call().await?; + + let receipt = receipt!(contract_alice.safeTransferFrom( + bob_addr, + alice_addr, + token_id, + value, + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferSingle { + operator: alice_addr, + from: bob_addr, + to: alice_addr, + id: token_id, + value + })); + + let Erc1155Supply::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr, token_id).call().await?; + assert_eq!(initial_alice_balance + value, alice_balance); + + let Erc1155Supply::balanceOfReturn { balance: bob_balance } = + contract_alice.balanceOf(bob_addr, token_id).call().await?; + assert_eq!(initial_bob_balance - value, bob_balance); + + Ok(()) +} + +#[e2e::test] +async fn safe_transfer_to_receiver_contract( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let receiver_addr = + receiver::deploy(&alice.wallet, ERC1155ReceiverMock::RevertType::None) + .await?; + + let alice_addr = alice.address(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let _ = watch!(contract.mint( + alice_addr, + token_id, + value, + vec![0, 1, 2, 3].into() + )); + + let Erc1155Supply::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr, token_id).call().await?; + let Erc1155Supply::balanceOfReturn { balance: initial_receiver_balance } = + contract.balanceOf(receiver_addr, token_id).call().await?; + + let receipt = receipt!(contract.safeTransferFrom( + alice_addr, + receiver_addr, + token_id, + value, + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferSingle { + operator: alice_addr, + from: alice_addr, + to: receiver_addr, + id: token_id, + value + })); + + assert!(receipt.emits(ERC1155ReceiverMock::Received { + operator: alice_addr, + from: alice_addr, + id: token_id, + value, + data: vec![].into(), + })); + + let Erc1155Supply::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr, token_id).call().await?; + assert_eq!(initial_alice_balance - value, alice_balance); + + let Erc1155Supply::balanceOfReturn { balance: receiver_balance } = + contract.balanceOf(receiver_addr, token_id).call().await?; + assert_eq!(initial_receiver_balance + value, receiver_balance); + + Ok(()) +} + +#[e2e::test] +async fn safe_batch_transfer_from( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract_alice = Erc1155Supply::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let token_ids = random_token_ids(2); + let values = random_values(2); + + let _ = watch!(contract_alice.mintBatch( + alice_addr, + token_ids.clone(), + values.clone(), + vec![].into() + )); + + let Erc1155Supply::balanceOfBatchReturn { + balances: initial_alice_balances, + } = contract_alice + .balanceOfBatch(vec![alice_addr, alice_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155Supply::balanceOfBatchReturn { balances: initial_bob_balances } = + contract_alice + .balanceOfBatch(vec![bob_addr, bob_addr], token_ids.clone()) + .call() + .await?; + + let receipt = receipt!(contract_alice.safeBatchTransferFrom( + alice_addr, + bob_addr, + token_ids.clone(), + values.clone(), + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferBatch { + operator: alice_addr, + from: alice_addr, + to: bob_addr, + ids: token_ids.clone(), + values: values.clone() + })); + + let Erc1155Supply::balanceOfBatchReturn { balances: alice_balances } = + contract_alice + .balanceOfBatch(vec![alice_addr, alice_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155Supply::balanceOfBatchReturn { balances: bob_balances } = + contract_alice + .balanceOfBatch(vec![bob_addr, bob_addr], token_ids.clone()) + .call() + .await?; + + for (idx, value) in values.iter().enumerate() { + assert_eq!(initial_alice_balances[idx] - value, alice_balances[idx]); + assert_eq!(initial_bob_balances[idx] + value, bob_balances[idx]); + } + + Ok(()) +} + +#[e2e::test] +async fn safe_batch_transfer_to_receiver_contract( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155Supply::new(contract_addr, &alice.wallet); + + let receiver_addr = + receiver::deploy(&alice.wallet, ERC1155ReceiverMock::RevertType::None) + .await?; + + let alice_addr = alice.address(); + let token_ids = random_token_ids(2); + let values = random_values(2); + + let _ = watch!(contract.mintBatch( + alice_addr, + token_ids.clone(), + values.clone(), + vec![].into() + )); + + let Erc1155Supply::balanceOfBatchReturn { + balances: initial_alice_balances, + } = contract + .balanceOfBatch(vec![alice_addr, alice_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155Supply::balanceOfBatchReturn { + balances: initial_receiver_balances, + } = contract + .balanceOfBatch(vec![receiver_addr, receiver_addr], token_ids.clone()) + .call() + .await?; + + let receipt = receipt!(contract.safeBatchTransferFrom( + alice_addr, + receiver_addr, + token_ids.clone(), + values.clone(), + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferBatch { + operator: alice_addr, + from: alice_addr, + to: receiver_addr, + ids: token_ids.clone(), + values: values.clone() + })); + + assert!(receipt.emits(ERC1155ReceiverMock::BatchReceived { + operator: alice_addr, + from: alice_addr, + ids: token_ids.clone(), + values: values.clone(), + data: vec![].into(), + })); + + let Erc1155Supply::balanceOfBatchReturn { balances: alice_balances } = + contract + .balanceOfBatch(vec![alice_addr, alice_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155Supply::balanceOfBatchReturn { balances: receiver_balances } = + contract + .balanceOfBatch( + vec![receiver_addr, receiver_addr], + token_ids.clone(), + ) + .call() + .await?; + + for (idx, value) in values.iter().enumerate() { + assert_eq!(initial_alice_balances[idx] - value, alice_balances[idx]); + assert_eq!( + initial_receiver_balances[idx] + value, + receiver_balances[idx] + ); + } + + Ok(()) +} + +#[e2e::test] +async fn safe_batch_transfer_from_with_approval( + alice: Account, + bob: Account, + dave: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract_alice = Erc1155Supply::new(contract_addr, &alice.wallet); + let contract_bob = Erc1155Supply::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let dave_addr = dave.address(); + let token_ids = random_token_ids(2); + let values = random_values(2); + + let _ = watch!(contract_alice.mintBatch( + bob_addr, + token_ids.clone(), + values.clone(), + vec![].into() + )); + + let _ = watch!(contract_bob.setApprovalForAll(alice_addr, true)); + + let Erc1155Supply::balanceOfBatchReturn { balances: initial_dave_balances } = + contract_alice + .balanceOfBatch(vec![dave_addr, dave_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155Supply::balanceOfBatchReturn { balances: initial_bob_balances } = + contract_alice + .balanceOfBatch(vec![bob_addr, bob_addr], token_ids.clone()) + .call() + .await?; + + let receipt = receipt!(contract_alice.safeBatchTransferFrom( + bob_addr, + dave_addr, + token_ids.clone(), + values.clone(), + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155Supply::TransferBatch { + operator: alice_addr, + from: bob_addr, + to: dave_addr, + ids: token_ids.clone(), + values: values.clone() + })); + + let Erc1155Supply::balanceOfBatchReturn { balances: bob_balances } = + contract_alice + .balanceOfBatch(vec![bob_addr, bob_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155Supply::balanceOfBatchReturn { balances: dave_balances } = + contract_alice + .balanceOfBatch(vec![dave_addr, dave_addr], token_ids.clone()) + .call() + .await?; + + for (idx, value) in values.iter().enumerate() { + assert_eq!(initial_bob_balances[idx] - value, bob_balances[idx]); + assert_eq!(initial_dave_balances[idx] + value, dave_balances[idx]); + } + + Ok(()) +} diff --git a/examples/erc1155-supply/tests/mock/mod.rs b/examples/erc1155-supply/tests/mock/mod.rs new file mode 100644 index 000000000..4c0db7eaa --- /dev/null +++ b/examples/erc1155-supply/tests/mock/mod.rs @@ -0,0 +1 @@ +pub mod receiver; diff --git a/examples/erc1155-supply/tests/mock/receiver.rs b/examples/erc1155-supply/tests/mock/receiver.rs new file mode 100644 index 000000000..6959954ab --- /dev/null +++ b/examples/erc1155-supply/tests/mock/receiver.rs @@ -0,0 +1,113 @@ +#![allow(dead_code)] +#![cfg(feature = "e2e")] +use alloy::{ + primitives::{Address, FixedBytes, U256}, + sol, +}; +use e2e::Wallet; +use stylus_sdk::{abi::Bytes, function_selector}; + +const REC_RETVAL: FixedBytes<4> = FixedBytes(function_selector!( + "onERC1155Received", + Address, + Address, + U256, + U256, + Bytes +)); + +const BAT_RETVAL: FixedBytes<4> = FixedBytes(function_selector!( + "onERC1155BatchReceived", + Address, + Address, + Vec, + Vec, + Bytes +)); + +sol! { + #[allow(missing_docs)] + // Built with Remix IDE; solc 0.8.24+commit.e11b9ed9 + #[sol(rpc, bytecode="60e060405234801562000010575f80fd5b5060405162000f7e38038062000f7e833981810160405281019062000036919062000181565b827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166080817bffffffffffffffffffffffffffffffffffffffffffffffffffffffff191681525050817bffffffffffffffffffffffffffffffffffffffffffffffffffffffff191660a0817bffffffffffffffffffffffffffffffffffffffffffffffffffffffff191681525050806004811115620000d857620000d7620001da565b5b60c0816004811115620000f057620000ef620001da565b5b8152505050505062000207565b5f80fd5b5f7fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b620001378162000101565b811462000142575f80fd5b50565b5f8151905062000155816200012c565b92915050565b6005811062000168575f80fd5b50565b5f815190506200017b816200015b565b92915050565b5f805f606084860312156200019b576200019a620000fd565b5b5f620001aa8682870162000145565b9350506020620001bd8682870162000145565b9250506040620001d0868287016200016b565b9150509250925092565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b60805160a05160c051610d0d620002715f395f8181610153015281816101a30152818161022a015281816102d2015281816103a4015281816103f40152818161047b015261052301525f61036001525f8181610262015281816104b301526105ad0152610d0d5ff3fe608060405234801561000f575f80fd5b506004361061003f575f3560e01c806301ffc9a714610043578063bc197c8114610073578063f23a6e61146100a3575b5f80fd5b61005d60048036038101906100589190610635565b6100d3565b60405161006a919061067a565b60405180910390f35b61008d600480360381019061008891906107a3565b61013c565b60405161009a9190610889565b60405180910390f35b6100bd60048036038101906100b891906108d5565b61038d565b6040516100ca9190610889565b60405180910390f35b5f7f01ffc9a7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916149050919050565b5f600160048111156101515761015061096b565b5b7f000000000000000000000000000000000000000000000000000000000000000060048111156101845761018361096b565b5b0361018d575f80fd5b600260048111156101a1576101a061096b565b5b7f000000000000000000000000000000000000000000000000000000000000000060048111156101d4576101d361096b565b5b03610214576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161020b90610a18565b60405180910390fd5b600360048111156102285761022761096b565b5b7f0000000000000000000000000000000000000000000000000000000000000000600481111561025b5761025a61096b565b5b036102bd577f00000000000000000000000000000000000000000000000000000000000000006040517f66435bc00000000000000000000000000000000000000000000000000000000081526004016102b49190610889565b60405180910390fd5b6004808111156102d0576102cf61096b565b5b7f000000000000000000000000000000000000000000000000000000000000000060048111156103035761030261096b565b5b03610319575f805f6103159190610a63565b9050505b7f9facaeece8596899cc39b65f0d1e262008ade8403076a2dfb6df2004fc8d96528989898989898989604051610356989796959493929190610b74565b60405180910390a17f0000000000000000000000000000000000000000000000000000000000000000905098975050505050505050565b5f600160048111156103a2576103a161096b565b5b7f000000000000000000000000000000000000000000000000000000000000000060048111156103d5576103d461096b565b5b036103de575f80fd5b600260048111156103f2576103f161096b565b5b7f000000000000000000000000000000000000000000000000000000000000000060048111156104255761042461096b565b5b03610465576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161045c90610c50565b60405180910390fd5b600360048111156104795761047861096b565b5b7f000000000000000000000000000000000000000000000000000000000000000060048111156104ac576104ab61096b565b5b0361050e577f00000000000000000000000000000000000000000000000000000000000000006040517f66435bc00000000000000000000000000000000000000000000000000000000081526004016105059190610889565b60405180910390fd5b6004808111156105215761052061096b565b5b7f000000000000000000000000000000000000000000000000000000000000000060048111156105545761055361096b565b5b0361056a575f805f6105669190610a63565b9050505b7fe4b060c773f3fcca980bf840b0e2856ca36598bb4da2c0c3913b89050630df378787878787876040516105a396959493929190610c7d565b60405180910390a17f000000000000000000000000000000000000000000000000000000000000000090509695505050505050565b5f80fd5b5f80fd5b5f7fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b610614816105e0565b811461061e575f80fd5b50565b5f8135905061062f8161060b565b92915050565b5f6020828403121561064a576106496105d8565b5b5f61065784828501610621565b91505092915050565b5f8115159050919050565b61067481610660565b82525050565b5f60208201905061068d5f83018461066b565b92915050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6106bc82610693565b9050919050565b6106cc816106b2565b81146106d6575f80fd5b50565b5f813590506106e7816106c3565b92915050565b5f80fd5b5f80fd5b5f80fd5b5f8083601f84011261070e5761070d6106ed565b5b8235905067ffffffffffffffff81111561072b5761072a6106f1565b5b602083019150836020820283011115610747576107466106f5565b5b9250929050565b5f8083601f840112610763576107626106ed565b5b8235905067ffffffffffffffff8111156107805761077f6106f1565b5b60208301915083600182028301111561079c5761079b6106f5565b5b9250929050565b5f805f805f805f8060a0898b0312156107bf576107be6105d8565b5b5f6107cc8b828c016106d9565b98505060206107dd8b828c016106d9565b975050604089013567ffffffffffffffff8111156107fe576107fd6105dc565b5b61080a8b828c016106f9565b9650965050606089013567ffffffffffffffff81111561082d5761082c6105dc565b5b6108398b828c016106f9565b9450945050608089013567ffffffffffffffff81111561085c5761085b6105dc565b5b6108688b828c0161074e565b92509250509295985092959890939650565b610883816105e0565b82525050565b5f60208201905061089c5f83018461087a565b92915050565b5f819050919050565b6108b4816108a2565b81146108be575f80fd5b50565b5f813590506108cf816108ab565b92915050565b5f805f805f8060a087890312156108ef576108ee6105d8565b5b5f6108fc89828a016106d9565b965050602061090d89828a016106d9565b955050604061091e89828a016108c1565b945050606061092f89828a016108c1565b935050608087013567ffffffffffffffff8111156109505761094f6105dc565b5b61095c89828a0161074e565b92509250509295509295509295565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602160045260245ffd5b5f82825260208201905092915050565b7f4552433131353552656365697665724d6f636b3a20726576657274696e67206f5f8201527f6e20626174636820726563656976650000000000000000000000000000000000602082015250565b5f610a02602f83610998565b9150610a0d826109a8565b604082019050919050565b5f6020820190508181035f830152610a2f816109f6565b9050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601260045260245ffd5b5f610a6d826108a2565b9150610a78836108a2565b925082610a8857610a87610a36565b5b828204905092915050565b610a9c816106b2565b82525050565b5f82825260208201905092915050565b5f80fd5b82818337505050565b5f610aca8385610aa2565b93507f07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff831115610afd57610afc610ab2565b5b602083029250610b0e838584610ab6565b82840190509392505050565b5f82825260208201905092915050565b828183375f83830152505050565b5f601f19601f8301169050919050565b5f610b538385610b1a565b9350610b60838584610b2a565b610b6983610b38565b840190509392505050565b5f60a082019050610b875f83018b610a93565b610b94602083018a610a93565b8181036040830152610ba781888a610abf565b90508181036060830152610bbc818688610abf565b90508181036080830152610bd1818486610b48565b90509998505050505050505050565b7f4552433131353552656365697665724d6f636b3a20726576657274696e67206f5f8201527f6e20726563656976650000000000000000000000000000000000000000000000602082015250565b5f610c3a602983610998565b9150610c4582610be0565b604082019050919050565b5f6020820190508181035f830152610c6781610c2e565b9050919050565b610c77816108a2565b82525050565b5f60a082019050610c905f830189610a93565b610c9d6020830188610a93565b610caa6040830187610c6e565b610cb76060830186610c6e565b8181036080830152610cca818486610b48565b905097965050505050505056fea26469706673582212208c442b680a6062015caa02f3c4c74cff54e26169331c5af35a3fa1703a3cc02364736f6c63430008180033")] + contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { + enum RevertType { + None, + RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, + Panic + } + + bytes4 private immutable _recRetval; + bytes4 private immutable _batRetval; + RevertType private immutable _error; + + #[derive(Debug, PartialEq)] + event Received(address operator, address from, uint256 id, uint256 value, bytes data); + + #[derive(Debug, PartialEq)] + event BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data); + + error CustomError(bytes4); + + constructor(bytes4 recRetval, bytes4 batRetval, RevertType error) { + _recRetval = recRetval; + _batRetval = batRetval; + _error = error; + } + + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4) { + if (_error == RevertType.RevertWithoutMessage) { + revert(); + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC1155ReceiverMock: reverting on receive"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_recRetval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + + emit Received(operator, from, id, value, data); + return _recRetval; + } + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4) { + if (_error == RevertType.RevertWithoutMessage) { + revert(); + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC1155ReceiverMock: reverting on batch receive"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_recRetval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + + emit BatchReceived(operator, from, ids, values, data); + return _batRetval; + } + } +} + +pub async fn deploy( + wallet: &Wallet, + error: ERC1155ReceiverMock::RevertType, +) -> eyre::Result
{ + let contract = + ERC1155ReceiverMock::deploy(wallet, REC_RETVAL, BAT_RETVAL, error) + .await?; + Ok(*contract.address()) +} diff --git a/examples/erc1155/tests/erc1155.rs b/examples/erc1155/tests/erc1155.rs index d9e27e9b0..90265bf74 100644 --- a/examples/erc1155/tests/erc1155.rs +++ b/examples/erc1155/tests/erc1155.rs @@ -13,7 +13,7 @@ fn random_token_ids(size: usize) -> Vec { } fn random_values(size: usize) -> Vec { - (1..size + 1).map(U256::from).collect() + (1..=size).map(U256::from).collect() } // ============================================================================