From 10690f162f83f53f0651b722f0f58c88167df4f8 Mon Sep 17 00:00:00 2001 From: Joseph Zhao <65984904+programskillforverification@users.noreply.github.com> Date: Fri, 8 Nov 2024 05:52:38 +0800 Subject: [PATCH] feat: ERC1155 (#275) Resolves #261 #### PR Checklist - [x] ERC1155 - [x] Tests --------- Co-authored-by: Alisander Qoshqosh Co-authored-by: Daniel Bigos Co-authored-by: Nenad --- CHANGELOG.md | 1 + Cargo.lock | 14 + Cargo.toml | 2 + benches/src/erc1155.rs | 104 + benches/src/lib.rs | 1 + contracts/src/lib.rs | 6 +- contracts/src/token/erc1155/mod.rs | 2195 ++++++++++++++++++ contracts/src/token/erc1155/receiver.rs | 59 + contracts/src/token/mod.rs | 1 + docs/modules/ROOT/nav.adoc | 2 + docs/modules/ROOT/pages/ERC1155.adoc | 3 + docs/modules/ROOT/pages/tokens.adoc | 3 +- examples/erc1155/Cargo.toml | 25 + examples/erc1155/src/ERC1155ReceiverMock.sol | 74 + examples/erc1155/src/constructor.sol | 7 + examples/erc1155/src/lib.rs | 45 + examples/erc1155/tests/abi/mod.rs | 31 + examples/erc1155/tests/erc1155.rs | 1570 +++++++++++++ examples/erc1155/tests/mock/mod.rs | 1 + examples/erc1155/tests/mock/receiver.rs | 97 + 20 files changed, 4239 insertions(+), 2 deletions(-) create mode 100644 benches/src/erc1155.rs create mode 100644 contracts/src/token/erc1155/mod.rs create mode 100644 contracts/src/token/erc1155/receiver.rs create mode 100644 docs/modules/ROOT/pages/ERC1155.adoc create mode 100644 examples/erc1155/Cargo.toml create mode 100644 examples/erc1155/src/ERC1155ReceiverMock.sol create mode 100644 examples/erc1155/src/constructor.sol create mode 100644 examples/erc1155/src/lib.rs create mode 100644 examples/erc1155/tests/abi/mod.rs create mode 100644 examples/erc1155/tests/erc1155.rs create mode 100644 examples/erc1155/tests/mock/mod.rs create mode 100644 examples/erc1155/tests/mock/receiver.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index fd754d3f5..94cc5d580 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 +- ERC-1155 Multi Token Standard. #275 - `SafeErc20` Utility. #289 - Finite Fields arithmetics. #376 diff --git a/Cargo.lock b/Cargo.lock index adc28056b..26a4862b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1547,6 +1547,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erc1155-example" +version = "0.1.1" +dependencies = [ + "alloy", + "alloy-primitives", + "e2e", + "eyre", + "openzeppelin-stylus", + "rand", + "stylus-sdk", + "tokio", +] + [[package]] name = "erc20-example" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 7c72877be..99def5a6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "examples/erc721", "examples/erc721-consecutive", "examples/erc721-metadata", + "examples/erc1155", "examples/merkle-proofs", "examples/ownable", "examples/access-control", @@ -33,6 +34,7 @@ default-members = [ "examples/erc721", "examples/erc721-consecutive", "examples/erc721-metadata", + "examples/erc1155", "examples/safe-erc20", "examples/merkle-proofs", "examples/ownable", diff --git a/benches/src/erc1155.rs b/benches/src/erc1155.rs new file mode 100644 index 000000000..4806a1155 --- /dev/null +++ b/benches/src/erc1155.rs @@ -0,0 +1,104 @@ +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 Erc1155 { + function balanceOf(address account, uint256 id) external view returns (uint256 balance); + 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; + } +); + +sol!("../examples/erc1155/src/constructor.sol"); + +pub async fn bench() -> eyre::Result { + let reports = run_with(CacheOpt::None).await?; + let report = reports + .into_iter() + .try_fold(ContractReport::new("Erc1155"), 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 bob = Account::new().await?; + let bob_addr = bob.address(); + + let contract_addr = deploy(&alice, cache_opt).await?; + + let contract = Erc1155::new(contract_addr, &alice_wallet); + + let token_1 = uint!(1_U256); + let token_2 = uint!(2_U256); + let token_3 = uint!(3_U256); + let token_4 = uint!(4_U256); + + let value_1 = uint!(100_U256); + let value_2 = uint!(200_U256); + let value_3 = uint!(300_U256); + let value_4 = uint!(400_U256); + + let ids = vec![token_1, token_2, token_3, token_4]; + let values = vec![value_1, value_2, value_3, value_4]; + + let data: alloy_primitives::Bytes = vec![].into(); + + // IMPORTANT: Order matters! + use Erc1155::*; + #[rustfmt::skip] + let receipts = vec![ + (mintCall::SIGNATURE, receipt!(contract.mint(alice_addr, token_1, value_1, data.clone()))?), + (mintBatchCall::SIGNATURE, receipt!(contract.mintBatch(alice_addr, ids.clone(), values.clone(), data.clone()))?), + (balanceOfCall::SIGNATURE, receipt!(contract.balanceOf(alice_addr, token_1))?), + (balanceOfBatchCall::SIGNATURE, receipt!(contract.balanceOfBatch(vec![alice_addr, bob_addr], vec![token_1, token_2]))?), + (setApprovalForAllCall::SIGNATURE, receipt!(contract.setApprovalForAll(bob_addr, true))?), + (isApprovedForAllCall::SIGNATURE, receipt!(contract.isApprovedForAll(alice_addr, bob_addr))?), + (safeTransferFromCall::SIGNATURE, receipt!(contract.safeTransferFrom(alice_addr, bob_addr, token_1, value_1, data.clone()))?), + (safeBatchTransferFromCall::SIGNATURE, receipt!(contract.safeBatchTransferFrom(alice_addr, bob_addr, ids, values, data.clone()))?) + ]; + + receipts + .into_iter() + .map(FunctionReport::new) + .collect::>>() +} + +async fn deploy( + account: &Account, + cache_opt: CacheOpt, +) -> eyre::Result
{ + crate::deploy(account, "erc1155", None, cache_opt).await +} diff --git a/benches/src/lib.rs b/benches/src/lib.rs index c884bcdb8..44e36faf4 100644 --- a/benches/src/lib.rs +++ b/benches/src/lib.rs @@ -14,6 +14,7 @@ use koba::config::{Deploy, Generate, PrivateKey}; use serde::Deserialize; pub mod access_control; +pub mod erc1155; pub mod erc20; pub mod erc721; pub mod merkle_proofs; diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 27b4eaa3d..a3a77988f 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -39,7 +39,11 @@ impl MyContract { } ``` */ -#![allow(clippy::pub_underscore_fields, clippy::module_name_repetitions)] +#![allow( + clippy::pub_underscore_fields, + clippy::module_name_repetitions, + clippy::used_underscore_items +)] #![cfg_attr(not(feature = "std"), no_std, no_main)] #![deny(rustdoc::broken_intra_doc_links)] extern crate alloc; diff --git a/contracts/src/token/erc1155/mod.rs b/contracts/src/token/erc1155/mod.rs new file mode 100644 index 000000000..e128f6868 --- /dev/null +++ b/contracts/src/token/erc1155/mod.rs @@ -0,0 +1,2195 @@ +//! Implementation of the ERC-1155 token standard. +use alloc::{vec, vec::Vec}; + +use alloy_primitives::{fixed_bytes, Address, FixedBytes, U256}; +use openzeppelin_stylus_proc::interface_id; +use stylus_sdk::{ + abi::Bytes, + alloy_sol_types::sol, + call::{self, Call, MethodError}, + evm, msg, + prelude::{public, sol_storage, AddressVM, SolidityError}, + storage::TopLevelStorage, +}; + +use crate::utils::{ + introspection::erc165::{Erc165, IErc165}, + math::storage::SubAssignUnchecked, +}; + +mod receiver; +pub use receiver::IERC1155Receiver; + +/// `bytes4( +/// keccak256( +/// "onERC1155Received(address,address,uint256,uint256,bytes)" +/// ))` +const SINGLE_TRANSFER_FN_SELECTOR: FixedBytes<4> = fixed_bytes!("f23a6e61"); + +/// `bytes4( +/// keccak256( +/// "onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)" +/// ))` +const BATCH_TRANSFER_FN_SELECTOR: FixedBytes<4> = fixed_bytes!("bc197c81"); + +sol! { + /// Emitted when `value` amount of tokens of type `id` are + /// transferred from `from` to `to` by `operator`. + #[allow(missing_docs)] + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 value + ); + + /// Equivalent to multiple [`TransferSingle`] events, where `operator` + /// `from` and `to` are the same for all transfers. + #[allow(missing_docs)] + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] values + ); + + /// Emitted when `account` grants or revokes permission to `operator` + /// to transfer their tokens, according to `approved`. + #[allow(missing_docs)] + event ApprovalForAll( + address indexed account, + address indexed operator, + bool approved + ); +} + +sol! { + /// Indicates an error related to the current `balance` of a `sender`. + /// Used in transfers. + /// + /// * `sender` - Address whose tokens are being transferred. + /// * `balance` - Current balance for the interacting account. + /// * `needed` - Minimum amount required to perform a transfer. + /// * `token_id` - Identifier number of a token. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC1155InsufficientBalance( + address sender, + uint256 balance, + uint256 needed, + uint256 token_id + ); + + /// Indicates a failure with the token `sender`. + /// Used in transfers. + /// + /// * `sender` - Address whose tokens are being transferred. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC1155InvalidSender(address sender); + + /// Indicates a failure with the token `receiver`. + /// Used in transfers. + /// + /// * `receiver` - Address to which tokens are being transferred. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC1155InvalidReceiver(address receiver); + + /// Indicates a failure with the `operator`’s approval. + /// Used in transfers. + /// + /// * `operator` - Address that may be allowed to operate on tokens + /// without being their owner. + /// * `owner` - Address of the current owner of a token. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC1155MissingApprovalForAll(address operator, address owner); + + /// Indicates a failure with the `approver` of a token to be approved. + /// Used in approvals. + /// + /// * `approver` - Address initiating an approval operation. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC1155InvalidApprover(address approver); + + /// Indicates a failure with the `operator` to be approved. + /// Used in approvals. + /// + /// * `operator` - Address that may be allowed to operate on tokens + /// without being their owner. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC1155InvalidOperator(address operator); + + /// Indicates an array length mismatch between token ids and values in a + /// [`IErc1155::safe_batch_transfer_from`] operation. + /// Used in batch transfers. + /// + /// * `ids_length` - Length of the array of token identifiers. + /// * `values_length` - Length of the array of token amounts. + #[derive(Debug)] + #[allow(missing_docs)] + error ERC1155InvalidArrayLength(uint256 ids_length, uint256 values_length); +} + +/// An [`Erc1155`] error defined as described in [ERC-6093]. +/// +/// [ERC-6093]: https://eips.ethereum.org/EIPS/eip-6093 +#[derive(SolidityError, Debug)] +pub enum Error { + /// Indicates an error related to the current `balance` of `sender`. + /// Used in transfers. + InsufficientBalance(ERC1155InsufficientBalance), + /// Indicates a failure with the token `sender`. Used in transfers. + InvalidSender(ERC1155InvalidSender), + /// Indicates a failure with the token `receiver`. Used in transfers. + InvalidReceiver(ERC1155InvalidReceiver), + /// Indicates a failure with the token `receiver`, with the reason + /// specified by it. + InvalidReceiverWithReason(call::Error), + /// Indicates a failure with the `operator`’s approval. Used in transfers. + MissingApprovalForAll(ERC1155MissingApprovalForAll), + /// Indicates a failure with the `approver` of a token to be approved. + /// Used in approvals. + InvalidApprover(ERC1155InvalidApprover), + /// Indicates a failure with the `operator` to be approved. Used in + /// approvals. + InvalidOperator(ERC1155InvalidOperator), + /// Indicates an array length mismatch between token ids and values in a + /// [`Erc1155::safe_batch_transfer_from`] operation. + /// Used in batch transfers. + InvalidArrayLength(ERC1155InvalidArrayLength), +} + +impl MethodError for Error { + fn encode(self) -> alloc::vec::Vec { + self.into() + } +} + +sol_storage! { + /// State of an [`Erc1155`] token. + pub struct Erc1155 { + /// Maps users to balances. + mapping(uint256 => mapping(address => uint256)) _balances; + /// Maps owners to a mapping of operator approvals. + mapping(address => mapping(address => bool)) _operator_approvals; + } +} + +/// NOTE: Implementation of [`TopLevelStorage`] to be able use `&mut self` when +/// calling other contracts and not `&mut (impl TopLevelStorage + +/// BorrowMut)`. Should be fixed in the future by the Stylus team. +unsafe impl TopLevelStorage for Erc1155 {} + +/// Required interface of an [`Erc1155`] compliant contract. +#[interface_id] +pub trait IErc1155 { + /// The error type associated to this ERC-1155 trait implementation. + type Error: Into>; + + /// Returns the value of tokens of type `id` owned by `account`. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `account` - Account of the token's owner. + /// * `id` - Token id as a number. + fn balance_of(&self, account: Address, id: U256) -> U256; + + /// Batched version of [`IErc1155::balance_of`]. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `accounts` - All account of the tokens' owner. + /// * `ids` - All token identifiers. + /// + /// # Requirements + /// + /// * `accounts` and `ids` must have the same length. + /// + /// # Errors + /// + /// * If the length of `accounts` is not equal to the length of `ids`, + /// then the error [`Error::InvalidArrayLength`] is returned. + fn balance_of_batch( + &self, + accounts: Vec
, + ids: Vec, + ) -> Result, Self::Error>; + + /// Grants or revokes permission to `operator` + /// to transfer the caller's tokens, according to `approved`. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `operator` - Account to add to the set of authorized operators. + /// * `approved` - Flag that determines whether or not permission will be + /// granted to `operator`. If true, this means `operator` will be allowed + /// to manage `msg::sender()`'s assets. + /// + /// # Errors + /// + /// * If `operator` is `Address::ZERO`, then the error + /// [`Error::InvalidOperator`] is returned. + /// + /// # Requirements + /// + /// * The `operator` cannot be the `Address::ZERO`. + /// + /// # Events + /// + /// Emits an [`ApprovalForAll`] event. + fn set_approval_for_all( + &mut self, + operator: Address, + approved: bool, + ) -> Result<(), Self::Error>; + + /// Returns true if `operator` is approved to transfer `account`'s + /// tokens. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `account` - Account of the token's owner. + /// * `operator` - Account to be checked. + fn is_approved_for_all(&self, account: Address, operator: Address) -> bool; + + /// Transfers a `value` amount of tokens of type `id` from `from` to + /// `to`. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account to transfer tokens from. + /// * `to` - Account of the recipient. + /// * `id` - Token id as a number. + /// * `value` - Amount of tokens to be transferred. + /// * `data` - Additional data with no specified format, sent in call to + /// `to`. + /// + /// # Errors + /// + /// If `to` is `Address::ZERO`, then the error + /// [`Error::InvalidReceiver`] is returned. + /// If `from` is `Address::ZERO`, then the error + /// [`Error::InvalidSender`] is returned. + /// If the `from` is not the caller (`msg::sender()`), + /// and the caller does not have the right to approve, then the error + /// [`Error::MissingApprovalForAll`] is returned. + /// If `value` is greater than the balance of the `from` account, + /// then the error [`Error::InsufficientBalance`] is returned. + /// If [`IERC1155Receiver::on_erc_1155_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// + /// # Requirements + /// + /// * `to` cannot be the `Address::ZERO`. + /// * If the caller is not `from`, it must have been approved to spend + /// `from`'s tokens via [`IErc1155::set_approval_for_all`]. + /// * `from` must have a balance of tokens of type `id` of at least `value` + /// amount. + /// * If `to` refers to a smart contract, it must implement + /// [`IERC1155Receiver::on_erc_1155_received`] and return the acceptance + /// value. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event. + /// + /// # Panics + /// + /// Should not panic. + fn safe_transfer_from( + &mut self, + from: Address, + to: Address, + id: U256, + value: U256, + data: Bytes, + ) -> Result<(), Self::Error>; + + /// Batched version of [`IErc1155::safe_transfer_from`]. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account to transfer tokens from. + /// * `to` - Account of the recipient. + /// * `ids` - Array of all tokens ids. + /// * `values` - Array of all amount of tokens to be transferred. + /// * `data` - Additional data with no specified format, sent in call to + /// `to`. + /// + /// # Errors + /// + /// If `to` is `Address::ZERO`, then the error + /// [`Error::InvalidReceiver`] is returned. + /// If `from` is `Address::ZERO`, then the error + /// [`Error::InvalidSender`] is returned. + /// If length of `ids` is not equal to length of `values`, then the + /// error [`Error::InvalidArrayLength`] is returned. + /// If `value` is greater than the balance of the `from` account, + /// then the error [`Error::InsufficientBalance`] is returned. + /// If the `from` is not the caller (`msg::sender()`), + /// and the caller does not have the right to approve, then the error + /// [`Error::MissingApprovalForAll`] is returned. + /// If [`IERC1155Receiver::on_erc_1155_batch_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// + /// # Requirements + /// + /// * `to` cannot be the `Address::ZERO`. + /// * If the caller is not `from`, it must have been approved to spend + /// `from`'s tokens via [`IErc1155::set_approval_for_all`]. + /// * `from` must have a balance of tokens being transferred of at least + /// transferred amount. + /// * `ids` and `values` must have the same length. + /// * If `to` refers to a smart contract, it must implement + /// [`IERC1155Receiver::on_erc_1155_batch_received`] and return the + /// acceptance magic value. + /// + /// # Events + /// + /// Emits either a [`TransferSingle`] or a [`TransferBatch`] event, + /// depending on the length of the array arguments. + /// + /// # Panics + /// + /// Should not panic. + fn safe_batch_transfer_from( + &mut self, + from: Address, + to: Address, + ids: Vec, + values: Vec, + data: Bytes, + ) -> Result<(), Self::Error>; +} + +#[public] +impl IErc1155 for Erc1155 { + type Error = Error; + + fn balance_of(&self, account: Address, id: U256) -> U256 { + self._balances.get(id).get(account) + } + + fn balance_of_batch( + &self, + accounts: Vec
, + ids: Vec, + ) -> Result, Self::Error> { + Self::require_equal_arrays_length(&ids, &accounts)?; + + let balances: Vec = accounts + .iter() + .zip(ids.iter()) + .map(|(account, token_id)| self.balance_of(*account, *token_id)) + .collect(); + + Ok(balances) + } + + fn set_approval_for_all( + &mut self, + operator: Address, + approved: bool, + ) -> Result<(), Self::Error> { + self._set_approval_for_all(msg::sender(), operator, approved) + } + + fn is_approved_for_all(&self, account: Address, operator: Address) -> bool { + self._operator_approvals.get(account).get(operator) + } + + fn safe_transfer_from( + &mut self, + from: Address, + to: Address, + id: U256, + value: U256, + data: Bytes, + ) -> Result<(), Self::Error> { + self.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<(), Self::Error> { + self.authorize_transfer(from)?; + self.do_safe_transfer_from(from, to, ids, values, &data) + } +} + +impl IErc165 for Erc1155 { + fn supports_interface(interface_id: FixedBytes<4>) -> bool { + ::INTERFACE_ID == u32::from_be_bytes(*interface_id) + || Erc165::supports_interface(interface_id) + } +} + +impl Erc1155 { + /// Transfers a `value` amount of tokens of type `ids` from `from` to + /// `to`. Will mint (or burn) if `from` (or `to`) is the `Address::ZERO`. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account to transfer tokens from. + /// * `to` - Account of the recipient. + /// * `ids` - Array of all tokens ids. + /// * `values` - Array of all amount of tokens to be transferred. + /// + /// # Errors + /// + /// If length of `ids` is not equal to length of `values`, then the + /// error [`Error::InvalidArrayLength`] is returned. + /// If `value` is greater than the balance of the `from` account, + /// then the error [`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 [`TransferSingle`] event if the arrays contain one element, and + /// [`TransferBatch`] otherwise. + /// + /// # Panics + /// + /// If updated balance exceeds `U256::MAX`, may happen during `mint` + /// operation. + fn _update( + &mut self, + from: Address, + to: Address, + ids: Vec, + values: Vec, + ) -> Result<(), Error> { + Self::require_equal_arrays_length(&ids, &values)?; + + let operator = msg::sender(); + + for (&token_id, &value) in ids.iter().zip(values.iter()) { + self.do_update(from, to, token_id, value)?; + } + + if ids.len() == 1 { + let id = ids[0]; + let value = values[0]; + evm::log(TransferSingle { operator, from, to, id, value }); + } else { + evm::log(TransferBatch { operator, from, to, ids, values }); + } + + Ok(()) + } + + /// Version of [`Self::_update`] that performs the token acceptance check by + /// calling [`IERC1155Receiver::on_erc_1155_received`] or + /// [`IERC1155Receiver::on_erc_1155_batch_received`] on the receiver address + /// if it contains code. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account to transfer tokens from. + /// * `to` - Account of the recipient. + /// * `ids` - Array of all token ids. + /// * `values` - Array of all amount of tokens to be transferred. + /// * `data` - Additional data with no specified format, sent in call to + /// `to`. + /// + /// # Errors + /// + /// If length of `ids` is not equal to length of `values`, then the + /// error [`Error::InvalidArrayLength`] is returned. + /// If `value` is greater than the balance of the `from` account, + /// then the error [`Error::InsufficientBalance`] is returned. + /// If [`IERC1155Receiver::on_erc_1155_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// If [`IERC1155Receiver::on_erc_1155_batch_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event if the arrays contain one element, and + /// [`TransferBatch`] otherwise. + /// + /// # Panics + /// + /// If updated balance exceeds `U256::MAX`, may happen during `mint` + /// operation. + fn _update_with_acceptance_check( + &mut self, + from: Address, + to: Address, + ids: Vec, + values: Vec, + data: &Bytes, + ) -> Result<(), Error> { + self._update(from, to, ids.clone(), values.clone())?; + + if !to.is_zero() { + self._check_on_erc1155_received( + msg::sender(), + from, + to, + Erc1155ReceiverData::new(ids, values), + data.to_vec().into(), + )?; + } + + Ok(()) + } + + /// Creates a `value` amount of tokens of type `id`, and assigns + /// them to `to`. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `to` - Account of the recipient. + /// * `id` - Token id. + /// * `value` - Amount of tokens to be minted. + /// * `data` - Additional data with no specified format, sent in call to + /// `to`. + /// + /// # Errors + /// + /// If `to` is `Address::ZERO`, then the error + /// [`Error::InvalidReceiver`] is returned. + /// If [`IERC1155Receiver::on_erc_1155_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event. + /// + /// # Panics + /// + /// If updated balance exceeds `U256::MAX`. + pub fn _mint( + &mut self, + to: Address, + id: U256, + value: U256, + data: &Bytes, + ) -> Result<(), Error> { + self._do_mint(to, vec![id], vec![value], data) + } + + /// Batched version of [`Self::_mint`]. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `to` - Account of the recipient. + /// * `ids` - Array of all tokens ids to be minted. + /// * `values` - Array of all amounts of tokens to be minted. + /// * `data` - Additional data with no specified format, sent in call to + /// `to`. + /// + /// # Errors + /// + /// If `to` is `Address::ZERO`, then the error + /// [`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 + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// If [`IERC1155Receiver::on_erc_1155_batch_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event if the arrays contain one element, and + /// [`TransferBatch`] otherwise. + /// + /// # Panics + /// + /// If updated balance exceeds `U256::MAX`. + pub fn _mint_batch( + &mut self, + to: Address, + ids: Vec, + values: Vec, + data: &Bytes, + ) -> Result<(), Error> { + self._do_mint(to, ids, values, data) + } + + /// Destroys a `value` amount of tokens of type `id` from `from`. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account to burn tokens from. + /// * `id` - Token id to be burnt. + /// * `value` - Amount of tokens to be burnt. + /// + /// # Errors + /// + /// If `from` is the `Address::ZERO`, then the error + /// [`Error::InvalidSender`] is returned. + /// If `value` is greater than the balance of the `from` account, + /// then the error [`Error::InsufficientBalance`] is returned. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event. + /// + /// # Panics + /// + /// Should not panic. + pub fn _burn( + &mut self, + from: Address, + id: U256, + value: U256, + ) -> Result<(), Error> { + self._do_burn(from, vec![id], vec![value]) + } + + /// Batched version of [`Self::_burn`]. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account to burn tokens from. + /// * `ids` - Array of all tokens ids to be burnt. + /// * `values` - Array of all amounts of tokens to be burnt. + /// + /// # Errors + /// + /// If `from` is the `Address::ZERO`, then the error + /// [`Error::InvalidSender`] is returned. + /// If length of `ids` is not equal to length of `values`, then the + /// error [`Error::InvalidArrayLength`] is returned. + /// If `value` is greater than the balance of the `from` account, + /// then the error [`Error::InsufficientBalance`] is returned. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event if the arrays contain one element, and + /// [`TransferBatch`] otherwise. + /// + /// # Panics + /// + /// Should not panic. + pub fn _burn_batch( + &mut self, + from: Address, + ids: Vec, + values: Vec, + ) -> Result<(), Error> { + self._do_burn(from, ids, values) + } + + /// Approve `operator` to operate on all of `owner` tokens. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `owner` - Tokens owner (`msg::sender`). + /// * `operator` - Account to add to the set of authorized operators. + /// * `approved` - Flag that determines whether or not permission will be + /// granted to `operator`. If true, this means `operator` will be allowed + /// to manage `owner`'s assets. + /// + /// # Errors + /// + /// If `operator` is the `Address::ZERO`, then the error + /// [`Error::InvalidOperator`] is returned. + /// + /// # Events + /// + /// Emits an [`ApprovalForAll`] event. + fn _set_approval_for_all( + &mut self, + owner: Address, + operator: Address, + approved: bool, + ) -> Result<(), Error> { + if operator.is_zero() { + return Err(Error::InvalidOperator(ERC1155InvalidOperator { + operator, + })); + } + self._operator_approvals.setter(owner).setter(operator).set(approved); + evm::log(ApprovalForAll { account: owner, operator, approved }); + Ok(()) + } +} + +impl Erc1155 { + /// Performs an acceptance check for the provided `operator` by calling + /// [`IERC1155Receiver::on_erc_1155_received`] in case of single token + /// transfer, or [`IERC1155Receiver::on_erc_1155_batch_received`] in + /// case of batch transfer on the `to` address. + /// + /// The acceptance call is not executed and treated as a no-op if the + /// target address doesn't contain code (i.e. an EOA). Otherwise, + /// the recipient must implement either + /// [`IERC1155Receiver::on_erc_1155_received`] for single transfer, or + /// [`IERC1155Receiver::on_erc_1155_batch_received`] for a batch transfer, + /// and return the acceptance value to accept the transfer. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `operator` - Generally the address that initiated the token transfer + /// (e.g. `msg::sender()`). + /// * `from` - Account of the sender. + /// * `to` - Account of the recipient. + /// * `details` - Details about token transfer, check + /// [`Erc1155ReceiverData`]. + /// * `data` - Additional data with no specified format, sent in call to + /// `to`. + /// + /// # Errors + /// + /// If [`IERC1155Receiver::on_erc_1155_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// If [`IERC1155Receiver::on_erc_1155_batch_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + fn _check_on_erc1155_received( + &mut self, + operator: Address, + from: Address, + to: Address, + details: Erc1155ReceiverData, + data: alloy_primitives::Bytes, + ) -> Result<(), Error> { + if !to.has_code() { + return Ok(()); + } + + let receiver = IERC1155Receiver::new(to); + let call = Call::new_in(self); + let result = match details.transfer { + Transfer::Single { id, value } => receiver + .on_erc_1155_received(call, operator, from, id, value, data), + + Transfer::Batch { ids, values } => receiver + .on_erc_1155_batch_received( + call, operator, from, ids, values, data, + ), + }; + + let id = match result { + Ok(id) => id, + Err(e) => { + if let call::Error::Revert(ref reason) = e { + if !reason.is_empty() { + // Non-IERC1155Receiver implementer. + return Err(Error::InvalidReceiverWithReason(e)); + } + } + + return Err(ERC1155InvalidReceiver { receiver: to }.into()); + } + }; + + // Token rejected. + if id != details.receiver_fn_selector { + return Err(ERC1155InvalidReceiver { receiver: to }.into()); + } + + Ok(()) + } + + /// Creates `values` of tokens specified by `ids`, and assigns + /// them to `to`. Performs the token acceptance check by + /// calling [`IERC1155Receiver::on_erc_1155_received`] or + /// [`IERC1155Receiver::on_erc_1155_batch_received`] on the `to` address + /// if it contains code. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `to` - Account of the recipient. + /// * `ids` - Array of all token ids to be minted. + /// * `values` - Array of all amounts of tokens to be minted. + /// * `data` - Additional data with no specified format, sent in call to + /// `to`. + /// + /// # Errors + /// + /// If `to` is `Address::ZERO`, then the error + /// [`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 + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// If [`IERC1155Receiver::on_erc_1155_batch_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event if the arrays contain one element, and + /// [`TransferBatch`] otherwise. + /// + /// # Panics + /// + /// If updated balance exceeds `U256::MAX`. + fn _do_mint( + &mut self, + to: Address, + ids: Vec, + values: Vec, + data: &Bytes, + ) -> Result<(), Error> { + if to.is_zero() { + return Err(Error::InvalidReceiver(ERC1155InvalidReceiver { + receiver: to, + })); + } + self._update_with_acceptance_check( + Address::ZERO, + to, + ids, + values, + data, + )?; + Ok(()) + } + + /// Destroys `values` amounts of tokens specified by `ids` from `from`. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account to burn tokens from. + /// * `ids` - Array of all token ids to be burnt. + /// * `values` - Array of all amount of tokens to be burnt. + /// + /// # Errors + /// + /// If `from` is the `Address::ZERO`, then the error + /// [`Error::InvalidSender`] is returned. + /// If length of `ids` is not equal to length of `values`, then the + /// error [`Error::InvalidArrayLength`] is returned. + /// If `value` is greater than the balance of the `from` account, + /// then the error [`Error::InsufficientBalance`] is returned. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event if the arrays contain one element, and + /// [`TransferBatch`] otherwise. + /// + /// # Panics + /// + /// Should not panic. + fn _do_burn( + &mut self, + from: Address, + ids: Vec, + values: Vec, + ) -> Result<(), Error> { + if from.is_zero() { + return Err(Error::InvalidSender(ERC1155InvalidSender { + sender: from, + })); + } + self._update_with_acceptance_check( + from, + Address::ZERO, + ids, + values, + &vec![].into(), + )?; + Ok(()) + } + + /// Transfers `values` of tokens specified by `ids` from `from` to `to`. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account to transfer tokens from. + /// * `to` - Account of the recipient. + /// * `ids` - Array of all token ids. + /// * `values` - Array of all amount of tokens to be transferred. + /// * `data` - Additional data with no specified format, sent in call to + /// `to`. + /// + /// # Errors + /// + /// If `to` is the `Address::ZERO`, then the error + /// [`Error::InvalidReceiver`] is returned. + /// If `from` is the `Address::ZERO`, then the error + /// [`Error::InvalidSender`] is returned. + /// If length of `ids` is not equal to length of `values`, then the + /// error [`Error::InvalidArrayLength`] is returned. + /// If `value` is greater than the balance of the `from` account, + /// then the error [`Error::InsufficientBalance`] is returned. + /// If [`IERC1155Receiver::on_erc_1155_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// If [`IERC1155Receiver::on_erc_1155_batch_received`] hasn't returned its + /// interface id or returned with error, then the error + /// [`Error::InvalidReceiver`] is returned. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event if the arrays contain one element, and + /// [`TransferBatch`] otherwise. + /// + /// # Panics + /// + /// If updated balance exceeds `U256::MAX`. + fn do_safe_transfer_from( + &mut self, + from: Address, + to: Address, + ids: Vec, + values: Vec, + data: &Bytes, + ) -> Result<(), Error> { + if to.is_zero() { + return Err(Error::InvalidReceiver(ERC1155InvalidReceiver { + receiver: to, + })); + } + if from.is_zero() { + return Err(Error::InvalidSender(ERC1155InvalidSender { + sender: from, + })); + } + self._update_with_acceptance_check(from, to, ids, values, data) + } + + /// Transfers a `value` amount of `token_id` from `from` to + /// `to`. Will mint (or burn) if `from` (or `to`) is the `Address::ZERO`. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account to transfer tokens from. + /// * `to` - Account of the recipient. + /// * `token_id` - Token id. + /// * `value` - Amount of tokens to be transferred. + /// + /// # Errors + /// + /// If `value` is greater than the balance of the `from` account, + /// then the error [`Error::InsufficientBalance`] is returned. + /// + /// # Panics + /// + /// If updated balance exceeds `U256::MAX`. + fn do_update( + &mut self, + from: Address, + to: Address, + token_id: U256, + value: U256, + ) -> Result<(), Error> { + if !from.is_zero() { + let from_balance = self.balance_of(from, token_id); + if from_balance < value { + return Err(Error::InsufficientBalance( + ERC1155InsufficientBalance { + sender: from, + balance: from_balance, + needed: value, + token_id, + }, + )); + } + self._balances + .setter(token_id) + .setter(from) + .sub_assign_unchecked(value); + } + + if !to.is_zero() { + let balance = self._balances.getter(token_id).get(to); + let new_balance = balance + .checked_add(value) + .expect("should not exceed `U256::MAX` for `_balances`"); + self._balances.setter(token_id).setter(to).set(new_balance); + } + + Ok(()) + } + + /// Checks if `ids` array has same length as `values` array. + /// + /// # Arguments + /// + /// * `ids` - array of `ids`. + /// * `values` - array of `values`. + /// + /// # Errors + /// + /// If length of `ids` is not equal to length of `values`, then the error + /// [`Error::InvalidArrayLength`] is returned. + fn require_equal_arrays_length( + ids: &[T], + values: &[U], + ) -> Result<(), Error> { + if ids.len() != values.len() { + return Err(Error::InvalidArrayLength(ERC1155InvalidArrayLength { + ids_length: U256::from(ids.len()), + values_length: U256::from(values.len()), + })); + } + Ok(()) + } + + /// Checks if `msg::sender()` is authorized to transfer tokens. + /// + /// # Arguments + /// + /// * `&self` - Read access to the contract's state. + /// * `from` - Account to transfer tokens from. + /// + /// # Errors + /// + /// If the `from` is not the caller (`msg::sender()`), + /// and the caller does not have the right to approve, then the error + /// [`Error::MissingApprovalForAll`] is returned. + fn authorize_transfer(&self, from: Address) -> Result<(), Error> { + let sender = msg::sender(); + if from != sender && !self.is_approved_for_all(from, sender) { + return Err(Error::MissingApprovalForAll( + ERC1155MissingApprovalForAll { operator: sender, owner: from }, + )); + } + + Ok(()) + } +} + +/// Data struct to be passed to a contract that +/// implements [`IERC1155Receiver`] interface. +struct Erc1155ReceiverData { + /// ERC-1155 Receiver function selector. + receiver_fn_selector: FixedBytes<4>, + /// Transfer details, either [`Transfer::Single`] or [`Transfer::Batch`]. + transfer: Transfer, +} + +impl Erc1155ReceiverData { + /// Creates a new instance based on transfer details. + /// Assumes that `ids` is not empty. + /// + /// If `ids` array has only 1 element, + /// it means that it is a [`Transfer::Single`]. + /// If `ids` array has many elements, + /// it means that it is a [`Transfer::Batch`]. + /// + /// NOTE: Does not check if `ids` length is equal to `values`. + /// + /// # Arguments + /// + /// * `ids` - Array of tokens ids being transferred. + /// * `values` - Array of all amount of tokens being transferred. + fn new(ids: Vec, values: Vec) -> Self { + if ids.len() == 1 { + Self::single(ids[0], values[0]) + } else { + Self::batch(ids, values) + } + } + + /// Creates a new instance for a [`Transfer::Single`]. + /// Check [`IERC1155Receiver::on_erc_1155_received`]. + /// + /// # Arguments + /// + /// * `id` - Token id being transferred. + /// * `value` - Amount of tokens being transferred. + fn single(id: U256, value: U256) -> Self { + Self { + receiver_fn_selector: SINGLE_TRANSFER_FN_SELECTOR, + transfer: Transfer::Single { id, value }, + } + } + + /// Creates a new instance for a [`Transfer::Batch`]. + /// Check [`IERC1155Receiver::on_erc_1155_batch_received`]. + /// + /// # Arguments + /// + /// * `ids` - Array of tokens ids being transferred. + /// * `values` - Array of all amount of tokens being transferred. + fn batch(ids: Vec, values: Vec) -> Self { + Self { + receiver_fn_selector: BATCH_TRANSFER_FN_SELECTOR, + transfer: Transfer::Batch { ids, values }, + } + } +} + +/// Struct representing token transfer details. +#[derive(Debug, PartialEq)] +enum Transfer { + /// Transfer of a single token. + /// + /// # Attributes + /// + /// * `id` - Token id being transferred. + /// * `value` - Amount of tokens being transferred. + Single { id: U256, value: U256 }, + /// Batch tokens transfer. + /// + /// # Attributes + /// + /// * `ids` - Array of tokens ids being transferred. + /// * `values` - Array of all amount of tokens being transferred. + Batch { ids: Vec, values: Vec }, +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::{address, uint, Address, U256}; + use stylus_sdk::msg; + + use super::{ + ERC1155InsufficientBalance, ERC1155InvalidArrayLength, + ERC1155InvalidOperator, ERC1155InvalidReceiver, ERC1155InvalidSender, + ERC1155MissingApprovalForAll, Erc1155, Erc1155ReceiverData, Error, + IErc1155, Transfer, BATCH_TRANSFER_FN_SELECTOR, + SINGLE_TRANSFER_FN_SELECTOR, + }; + use crate::utils::introspection::erc165::IErc165; + + const ALICE: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); + const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526"); + const DAVE: Address = address!("0BB78F7e7132d1651B4Fd884B7624394e92156F1"); + const CHARLIE: Address = + address!("B0B0cB49ec2e96DF5F5fFB081acaE66A2cBBc2e2"); + + pub(crate) fn random_token_ids(size: usize) -> Vec { + (0..size).map(U256::from).collect() + } + + pub(crate) fn random_values(size: usize) -> Vec { + (0..size).map(|_| U256::from(rand::random::())).collect() + } + + fn init( + contract: &mut Erc1155, + 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![0, 1, 2, 3].into(), + ) + .expect("Mint failed"); + (token_ids, values) + } + + fn append(values: Vec, value: u64) -> Vec { + values.into_iter().chain(std::iter::once(U256::from(value))).collect() + } + + #[test] + fn should_create_transfer_single() { + let id = uint!(1_U256); + let value = uint!(10_U256); + let details = Erc1155ReceiverData::new(vec![id], vec![value]); + assert_eq!(SINGLE_TRANSFER_FN_SELECTOR, details.receiver_fn_selector); + assert_eq!(Transfer::Single { id, value }, details.transfer); + } + + #[test] + fn should_create_transfer_batch() { + let ids = random_token_ids(5); + let values = random_values(5); + let details = Erc1155ReceiverData::new(ids.clone(), values.clone()); + assert_eq!(BATCH_TRANSFER_FN_SELECTOR, details.receiver_fn_selector); + assert_eq!(Transfer::Batch { ids, values }, details.transfer); + } + + #[motsu::test] + fn balance_of_zero_balance(contract: Erc1155) { + let owner = msg::sender(); + let token_id = random_token_ids(1)[0]; + let balance = contract.balance_of(owner, token_id); + assert_eq!(U256::ZERO, balance); + } + + #[motsu::test] + fn error_when_array_length_mismatch(contract: Erc1155) { + let token_ids = random_token_ids(3); + let accounts = vec![ALICE, BOB, DAVE, CHARLIE]; + let ids_length = U256::from(token_ids.len()); + let accounts_length = U256::from(accounts.len()); + + let err = contract + .balance_of_batch(accounts, token_ids) + .expect_err("should return `Error::InvalidArrayLength`"); + + assert!(matches!( + err, + Error::InvalidArrayLength(ERC1155InvalidArrayLength { + ids_length: ids_l, + values_length: accounts_l, + }) if ids_l == ids_length && accounts_l == accounts_length + )); + } + + #[motsu::test] + fn balance_of_batch_zero_balance(contract: Erc1155) { + let token_ids = random_token_ids(4); + let accounts = vec![ALICE, BOB, DAVE, CHARLIE]; + let balances = contract + .balance_of_batch(accounts, token_ids) + .expect("should return a vector of `U256::ZERO`"); + + let expected = vec![U256::ZERO; 4]; + assert_eq!(expected, balances); + } + + #[motsu::test] + fn set_approval_for_all(contract: Erc1155) { + let alice = msg::sender(); + contract._operator_approvals.setter(alice).setter(BOB).set(false); + + contract + .set_approval_for_all(BOB, true) + .expect("should approve Bob for operations on all Alice's tokens"); + assert_eq!(contract.is_approved_for_all(alice, BOB), true); + + contract.set_approval_for_all(BOB, false).expect( + "should disapprove Bob for operations on all Alice's tokens", + ); + assert_eq!(contract.is_approved_for_all(alice, BOB), false); + } + + #[motsu::test] + fn error_when_invalid_operator_set_approval_for_all(contract: Erc1155) { + let invalid_operator = Address::ZERO; + + let err = contract + .set_approval_for_all(invalid_operator, true) + .expect_err("should not approve for all for invalid operator"); + + assert!(matches!( + err, + Error::InvalidOperator(ERC1155InvalidOperator { + operator + }) if operator == invalid_operator + )); + } + + #[motsu::test] + fn mints(contract: Erc1155) { + let alice = msg::sender(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + contract + ._mint(alice, token_id, value, &vec![0, 1, 2, 3].into()) + .expect("should mint tokens for Alice"); + + let balance = contract.balance_of(alice, token_id); + + assert_eq!(balance, value); + } + + #[motsu::test] + fn error_when_mints_to_invalid_receiver(contract: Erc1155) { + let invalid_receiver = Address::ZERO; + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let err = contract + ._mint(invalid_receiver, token_id, value, &vec![0, 1, 2, 3].into()) + .expect_err("should not mint tokens for invalid receiver"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { + receiver + }) if receiver == invalid_receiver + )); + } + + #[motsu::test] + fn mints_batch(contract: Erc1155) { + let token_ids = random_token_ids(4); + let values = random_values(4); + + contract + ._mint_batch( + ALICE, + token_ids.clone(), + values.clone(), + &vec![0, 1, 2, 3].into(), + ) + .expect("should batch mint tokens"); + + token_ids.iter().zip(values.iter()).for_each(|(&token_id, &value)| { + assert_eq!(value, contract.balance_of(ALICE, token_id)); + }); + + let balances = contract + .balance_of_batch(vec![ALICE; 4], token_ids.clone()) + .expect("should return balances"); + + assert_eq!(values, balances); + } + + #[motsu::test] + fn mints_batch_same_token(contract: Erc1155) { + let token_id = uint!(1_U256); + let values = random_values(4); + let expected_balance: U256 = values.iter().sum(); + + contract + ._mint_batch( + ALICE, + vec![token_id; 4], + values.clone(), + &vec![0, 1, 2, 3].into(), + ) + .expect("should batch mint tokens"); + + assert_eq!(expected_balance, contract.balance_of(ALICE, token_id)); + + let balances = contract + .balance_of_batch(vec![ALICE; 4], vec![token_id; 4]) + .expect("should return balances"); + + assert_eq!(vec![expected_balance; 4], balances); + } + + #[motsu::test] + fn error_when_batch_mints_to_invalid_receiver(contract: Erc1155) { + let token_ids = random_token_ids(1); + let values = random_values(1); + let invalid_receiver = Address::ZERO; + + let err = contract + ._mint_batch( + invalid_receiver, + token_ids, + values, + &vec![0, 1, 2, 3].into(), + ) + .expect_err("should not batch mint tokens for invalid receiver"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { + receiver + }) if receiver == invalid_receiver + )); + } + + #[motsu::test] + fn error_when_batch_mints_not_equal_arrays(contract: Erc1155) { + let token_ids = random_token_ids(3); + let values = random_values(4); + + let err = contract + ._mint_batch(ALICE, token_ids, values, &vec![0, 1, 2, 3].into()) + .expect_err( + "should not batch mint tokens when not equal array lengths", + ); + + assert!(matches!( + err, + Error::InvalidArrayLength(ERC1155InvalidArrayLength { + ids_length, values_length + }) if ids_length == uint!(3_U256) && values_length == uint!(4_U256) + )); + } + + #[motsu::test] + fn burns(contract: Erc1155) { + let (token_ids, values) = init(contract, ALICE, 1); + let token_id = token_ids[0]; + let value = values[0]; + + contract._burn(ALICE, token_id, value).expect("should burn tokens"); + + let balances = contract.balance_of(ALICE, token_id); + + assert_eq!(U256::ZERO, balances); + } + + #[motsu::test] + fn error_when_burns_from_invalid_sender(contract: Erc1155) { + 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 error_when_burns_with_insufficient_balance(contract: Erc1155) { + let (token_ids, values) = init(contract, ALICE, 1); + + let err = contract + ._burn(ALICE, token_ids[0], values[0] + uint!(1_U256)) + .expect_err("should not burn token when insufficient balance"); + + assert!(matches!( + err, + Error::InsufficientBalance(ERC1155InsufficientBalance { + sender, + balance, + needed, + token_id + }) if sender == ALICE && balance == values[0] && needed == values[0] + uint!(1_U256) && token_id == token_ids[0] + )); + } + + #[motsu::test] + fn burns_batch(contract: Erc1155) { + let (token_ids, values) = init(contract, ALICE, 4); + + contract + ._burn_batch(ALICE, token_ids.clone(), values.clone()) + .expect("should batch burn tokens"); + + let balances = contract + .balance_of_batch(vec![ALICE; 4], token_ids.clone()) + .expect("should return balances"); + + assert_eq!(vec![U256::ZERO; 4], balances); + } + + #[motsu::test] + fn burns_batch_same_token(contract: Erc1155) { + let token_id = uint!(1_U256); + let value = uint!(80_U256); + + contract + ._mint(ALICE, token_id, value, &vec![0, 1, 2, 3].into()) + .expect("should mint token"); + + contract + ._burn_batch( + ALICE, + vec![token_id; 4], + vec![ + uint!(20_U256), + uint!(10_U256), + uint!(30_U256), + uint!(20_U256), + ], + ) + .expect("should batch burn tokens"); + + assert_eq!(U256::ZERO, contract.balance_of(ALICE, token_id)); + } + + #[motsu::test] + fn error_when_batch_burns_from_invalid_sender(contract: Erc1155) { + let (token_ids, values) = init(contract, ALICE, 4); + let invalid_sender = Address::ZERO; + + let err = contract + ._burn_batch(invalid_sender, token_ids, values) + .expect_err("should not batch burn tokens for invalid sender"); + + assert!(matches!( + err, + Error::InvalidSender(ERC1155InvalidSender { + sender + }) if sender == invalid_sender + )); + } + + #[motsu::test] + fn error_when_batch_burns_with_insufficient_balance(contract: Erc1155) { + let (token_ids, values) = init(contract, ALICE, 4); + + let err = contract + ._burn_batch( + ALICE, + token_ids.clone(), + values.clone().into_iter().map(|x| x + uint!(1_U256)).collect(), + ) + .expect_err( + "should not batch burn tokens when insufficient balance", + ); + + assert!(matches!( + err, + Error::InsufficientBalance(ERC1155InsufficientBalance { + sender, + balance, + needed, + token_id + }) if sender == ALICE && balance == values[0] && needed == values[0] + uint!(1_U256) && token_id == token_ids[0] + )); + } + + #[motsu::test] + fn error_when_batch_burns_not_equal_arrays(contract: Erc1155) { + let (token_ids, values) = init(contract, ALICE, 3); + + let err = contract + ._burn_batch(ALICE, token_ids, append(values, 4)) + .expect_err( + "should not batch burn tokens when not equal array lengths", + ); + + assert!(matches!( + err, + Error::InvalidArrayLength(ERC1155InvalidArrayLength { + ids_length, values_length + }) if ids_length == uint!(3_U256) && values_length == uint!(4_U256) + )); + } + + #[motsu::test] + fn safe_transfer_from(contract: Erc1155) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, BOB, 2); + let amount_one = values[0] - uint!(1_U256); + let amount_two = values[1] - uint!(1_U256); + + contract._operator_approvals.setter(BOB).setter(alice).set(true); + + contract + .safe_transfer_from( + BOB, + DAVE, + token_ids[0], + amount_one, + vec![].into(), + ) + .expect("should transfer tokens from Alice to Bob"); + contract + .safe_transfer_from( + BOB, + DAVE, + token_ids[1], + amount_two, + vec![].into(), + ) + .expect("should transfer tokens from Alice to Bob"); + + let balance_id_one = contract.balance_of(DAVE, token_ids[0]); + let balance_id_two = contract.balance_of(DAVE, token_ids[1]); + + assert_eq!(amount_one, balance_id_one); + assert_eq!(amount_two, balance_id_two); + } + + #[motsu::test] + fn error_when_invalid_receiver_safe_transfer_from(contract: Erc1155) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, alice, 1); + let invalid_receiver = Address::ZERO; + + let err = contract + .safe_transfer_from( + alice, + invalid_receiver, + token_ids[0], + values[0], + vec![].into(), + ) + .expect_err("should not transfer tokens to the `Address::ZERO`"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { + receiver + }) if receiver == invalid_receiver + )); + } + + #[motsu::test] + fn error_when_invalid_sender_safe_transfer_from(contract: Erc1155) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, alice, 1); + let invalid_sender = Address::ZERO; + + contract + ._operator_approvals + .setter(invalid_sender) + .setter(alice) + .set(true); + + let err = contract + .safe_transfer_from( + invalid_sender, + BOB, + token_ids[0], + values[0], + vec![].into(), + ) + .expect_err("should not transfer tokens from the `Address::ZERO`"); + + assert!(matches!( + err, + Error::InvalidSender(ERC1155InvalidSender { + sender + }) if sender == invalid_sender + )); + } + + #[motsu::test] + fn error_when_missing_approval_safe_transfer_from(contract: Erc1155) { + let (token_ids, values) = init(contract, ALICE, 1); + + let err = contract + .safe_transfer_from( + ALICE, + BOB, + token_ids[0], + values[0], + vec![].into(), + ) + .expect_err("should not transfer tokens without approval"); + + assert!(matches!( + err, + Error::MissingApprovalForAll(ERC1155MissingApprovalForAll { + operator, + owner + }) if operator == msg::sender() && owner == ALICE + )); + } + + #[motsu::test] + fn error_when_insufficient_balance_safe_transfer_from(contract: Erc1155) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, BOB, 1); + + contract._operator_approvals.setter(BOB).setter(alice).set(true); + + let err = contract + .safe_transfer_from( + BOB, + DAVE, + token_ids[0], + values[0] + uint!(1_U256), + vec![].into(), + ) + .expect_err("should not transfer tokens with insufficient balance"); + + assert!(matches!( + err, + Error::InsufficientBalance(ERC1155InsufficientBalance { + sender, + balance, + needed, + token_id + }) if sender == BOB && balance == values[0] && needed == values[0] + uint!(1_U256) && token_id == token_ids[0] + )); + } + + #[motsu::test] + fn safe_transfer_from_with_data(contract: Erc1155) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, DAVE, 1); + + contract._operator_approvals.setter(DAVE).setter(alice).set(true); + + contract + .safe_transfer_from( + DAVE, + CHARLIE, + token_ids[0], + values[0], + vec![0, 1, 2, 3].into(), + ) + .expect("should transfer tokens from Alice to Bob"); + + let balance = contract.balance_of(CHARLIE, token_ids[0]); + + assert_eq!(values[0], balance); + } + + #[motsu::test] + fn error_when_invalid_receiver_safe_transfer_from_with_data( + contract: Erc1155, + ) { + let (token_ids, values) = init(contract, DAVE, 1); + let invalid_receiver = Address::ZERO; + + let err = contract + .do_safe_transfer_from( + DAVE, + invalid_receiver, + token_ids, + values, + &vec![0, 1, 2, 3].into(), + ) + .expect_err("should not transfer tokens to the `Address::ZERO`"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { + receiver + }) if receiver == invalid_receiver + )); + } + + #[motsu::test] + fn error_when_invalid_sender_safe_transfer_from_with_data( + contract: Erc1155, + ) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, alice, 1); + let invalid_sender = Address::ZERO; + + contract + ._operator_approvals + .setter(invalid_sender) + .setter(alice) + .set(true); + + let err = contract + .safe_transfer_from( + invalid_sender, + CHARLIE, + token_ids[0], + values[0], + vec![0, 1, 2, 3].into(), + ) + .expect_err("should not transfer tokens from the `Address::ZERO`"); + + assert!(matches!( + err, + Error::InvalidSender(ERC1155InvalidSender { + sender + }) if sender == invalid_sender + )); + } + + #[motsu::test] + fn error_when_missing_approval_safe_transfer_from_with_data( + contract: Erc1155, + ) { + let (token_ids, values) = init(contract, ALICE, 1); + + let err = contract + .safe_transfer_from( + ALICE, + BOB, + token_ids[0], + values[0], + vec![0, 1, 2, 3].into(), + ) + .expect_err("should not transfer tokens without approval"); + + assert!(matches!( + err, + Error::MissingApprovalForAll(ERC1155MissingApprovalForAll { + operator, + owner + }) if operator == msg::sender() && owner == ALICE + )); + } + + #[motsu::test] + fn error_when_insufficient_balance_safe_transfer_from_with_data( + contract: Erc1155, + ) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, BOB, 1); + + contract._operator_approvals.setter(BOB).setter(alice).set(true); + + let err = contract + .safe_transfer_from( + BOB, + DAVE, + token_ids[0], + values[0] + uint!(1_U256), + vec![0, 1, 2, 3].into(), + ) + .expect_err("should not transfer tokens with insufficient balance"); + + assert!(matches!( + err, + Error::InsufficientBalance(ERC1155InsufficientBalance { + sender, + balance, + needed, + token_id + }) if sender == BOB && balance == values[0] && needed == values[0] + uint!(1_U256) && token_id == token_ids[0] + )); + } + + #[motsu::test] + fn safe_batch_transfer_from(contract: Erc1155) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, DAVE, 2); + let amount_one = values[0] - uint!(1_U256); + let amount_two = values[1] - uint!(1_U256); + + contract._operator_approvals.setter(DAVE).setter(alice).set(true); + + contract + .safe_batch_transfer_from( + DAVE, + BOB, + token_ids.clone(), + vec![amount_one, amount_two], + vec![].into(), + ) + .expect("should transfer tokens from Alice to Bob"); + + let balance_id_one = contract.balance_of(BOB, token_ids[0]); + let balance_id_two = contract.balance_of(BOB, token_ids[1]); + + assert_eq!(amount_one, balance_id_one); + assert_eq!(amount_two, balance_id_two); + } + + #[motsu::test] + fn error_when_invalid_receiver_safe_batch_transfer_from(contract: Erc1155) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, alice, 4); + let invalid_receiver = Address::ZERO; + + let err = contract + .safe_batch_transfer_from( + alice, + invalid_receiver, + token_ids.clone(), + values.clone(), + vec![].into(), + ) + .expect_err("should not transfer tokens to the `Address::ZERO`"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { + receiver + }) if receiver == invalid_receiver + )); + } + + #[motsu::test] + fn error_when_invalid_sender_safe_batch_transfer_from(contract: Erc1155) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, alice, 4); + let invalid_sender = Address::ZERO; + + contract + ._operator_approvals + .setter(invalid_sender) + .setter(alice) + .set(true); + + let err = contract + .safe_batch_transfer_from( + invalid_sender, + CHARLIE, + token_ids.clone(), + values.clone(), + vec![].into(), + ) + .expect_err("should not transfer tokens from the `Address::ZERO`"); + + assert!(matches!( + err, + Error::InvalidSender(ERC1155InvalidSender { + sender + }) if sender == invalid_sender + )); + } + + #[motsu::test] + fn error_when_missing_approval_safe_batch_transfer_from(contract: Erc1155) { + let (token_ids, values) = init(contract, ALICE, 2); + + let err = contract + .safe_batch_transfer_from( + ALICE, + BOB, + token_ids.clone(), + values.clone(), + vec![].into(), + ) + .expect_err("should not transfer tokens without approval"); + + assert!(matches!( + err, + Error::MissingApprovalForAll(ERC1155MissingApprovalForAll { + operator, + owner + }) if operator == msg::sender() && owner == ALICE + )); + } + + #[motsu::test] + fn error_when_insufficient_balance_safe_batch_transfer_from( + contract: Erc1155, + ) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, CHARLIE, 2); + + contract._operator_approvals.setter(CHARLIE).setter(alice).set(true); + + let err = contract + .safe_batch_transfer_from( + CHARLIE, + BOB, + token_ids.clone(), + vec![values[0] + uint!(1_U256), values[1]], + vec![].into(), + ) + .expect_err("should not transfer tokens with insufficient balance"); + + assert!(matches!( + err, + Error::InsufficientBalance(ERC1155InsufficientBalance { + sender, + balance, + needed, + token_id + }) if sender == CHARLIE && balance == values[0] && needed == values[0] + uint!(1_U256) && token_id == token_ids[0] + )); + } + + #[motsu::test] + fn error_when_not_equal_arrays_safe_batch_transfer_from(contract: Erc1155) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, alice, 4); + + contract._operator_approvals.setter(DAVE).setter(alice).set(true); + + let err = contract + .safe_batch_transfer_from( + DAVE, + CHARLIE, + token_ids.clone(), + append(values, 4), + vec![].into(), + ) + .expect_err( + "should not transfer tokens when not equal array lengths", + ); + + assert!(matches!( + err, + Error::InvalidArrayLength(ERC1155InvalidArrayLength { + ids_length, values_length + }) if ids_length == uint!(4_U256) && values_length == uint!(5_U256) + )); + } + + #[motsu::test] + fn safe_batch_transfer_from_with_data(contract: Erc1155) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, DAVE, 2); + + contract._operator_approvals.setter(DAVE).setter(alice).set(true); + + contract + .safe_batch_transfer_from( + DAVE, + BOB, + token_ids.clone(), + values.clone(), + vec![0, 1, 2, 3].into(), + ) + .expect("should transfer tokens from Alice to Bob"); + + let balance_id_one = contract.balance_of(BOB, token_ids[0]); + let balance_id_two = contract.balance_of(BOB, token_ids[1]); + + assert_eq!(values[0], balance_id_one); + assert_eq!(values[1], balance_id_two); + } + + #[motsu::test] + fn error_when_invalid_receiver_safe_batch_transfer_from_with_data( + contract: Erc1155, + ) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, alice, 4); + let invalid_receiver = Address::ZERO; + + let err = contract + .safe_batch_transfer_from( + alice, + invalid_receiver, + token_ids.clone(), + values.clone(), + vec![0, 1, 2, 3].into(), + ) + .expect_err("should not transfer tokens to the `Address::ZERO`"); + + assert!(matches!( + err, + Error::InvalidReceiver(ERC1155InvalidReceiver { + receiver + }) if receiver == invalid_receiver + )); + } + + #[motsu::test] + fn error_when_invalid_sender_safe_batch_transfer_from_with_data( + contract: Erc1155, + ) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, alice, 4); + let invalid_sender = Address::ZERO; + + contract + ._operator_approvals + .setter(invalid_sender) + .setter(alice) + .set(true); + + let err = contract + .safe_batch_transfer_from( + invalid_sender, + CHARLIE, + token_ids.clone(), + values.clone(), + vec![0, 1, 2, 3].into(), + ) + .expect_err("should not transfer tokens from the `Address::ZERO`"); + + assert!(matches!( + err, + Error::InvalidSender(ERC1155InvalidSender { + sender + }) if sender == invalid_sender + )); + } + + #[motsu::test] + fn error_when_missing_approval_safe_batch_transfer_from_with_data( + contract: Erc1155, + ) { + let (token_ids, values) = init(contract, ALICE, 2); + + let err = contract + .safe_batch_transfer_from( + ALICE, + BOB, + token_ids.clone(), + values.clone(), + vec![0, 1, 2, 3].into(), + ) + .expect_err("should not transfer tokens without approval"); + + assert!(matches!( + err, + Error::MissingApprovalForAll(ERC1155MissingApprovalForAll { + operator, + owner + }) if operator == msg::sender() && owner == ALICE + )); + } + + #[motsu::test] + fn error_when_insufficient_balance_safe_batch_transfer_from_with_data( + contract: Erc1155, + ) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, CHARLIE, 2); + + contract._operator_approvals.setter(CHARLIE).setter(alice).set(true); + + let err = contract + .safe_batch_transfer_from( + CHARLIE, + BOB, + token_ids.clone(), + vec![values[0] + uint!(1_U256), values[1]], + vec![0, 1, 2, 3].into(), + ) + .expect_err("should not transfer tokens with insufficient balance"); + + assert!(matches!( + err, + Error::InsufficientBalance(ERC1155InsufficientBalance { + sender, + balance, + needed, + token_id + }) if sender == CHARLIE && balance == values[0] && needed == values[0] + uint!(1_U256) && token_id == token_ids[0] + )); + } + + #[motsu::test] + fn error_when_not_equal_arrays_safe_batch_transfer_from_with_data( + contract: Erc1155, + ) { + let alice = msg::sender(); + let (token_ids, values) = init(contract, alice, 4); + + contract._operator_approvals.setter(DAVE).setter(alice).set(true); + + let err = contract + .safe_batch_transfer_from( + DAVE, + CHARLIE, + token_ids.clone(), + append(values, 4), + vec![0, 1, 2, 3].into(), + ) + .expect_err( + "should not transfer tokens when not equal array lengths", + ); + + assert!(matches!( + err, + Error::InvalidArrayLength(ERC1155InvalidArrayLength { + ids_length, values_length + }) if ids_length == uint!(4_U256) && values_length == uint!(5_U256) + )); + } + + #[motsu::test] + fn interface_id() { + let actual = ::INTERFACE_ID; + let expected = 0xd9b67a26; + assert_eq!(actual, expected); + + let actual = ::INTERFACE_ID; + let expected = 0x01ffc9a7; + assert_eq!(actual, expected); + } +} diff --git a/contracts/src/token/erc1155/receiver.rs b/contracts/src/token/erc1155/receiver.rs new file mode 100644 index 000000000..49da4798f --- /dev/null +++ b/contracts/src/token/erc1155/receiver.rs @@ -0,0 +1,59 @@ +#![allow(missing_docs)] +//! Module with an interface required for smart contract +//! in order to receive ERC-1155 token transfers. + +use stylus_sdk::stylus_proc::sol_interface; + +sol_interface! { + /// Interface that must be implemented by smart contracts + /// in order to receive ERC-1155 token transfers. + interface IERC1155Receiver { + /// Handles the receipt of a single ERC-1155 token type. + /// This function is called at the end of a + /// [`IErc1155::safe_batch_transfer_from`] + /// after the balance has been updated. + /// + /// NOTE: To accept the transfer, + /// this must return [`SINGLE_TRANSFER_FN_SELECTOR`], + /// or its own function selector. + /// + /// * `operator` - The address which initiated the transfer. + /// * `from` - The address which previously owned the token. + /// * `id` - The ID of the token being transferred. + /// * `value` - The amount of tokens being transferred. + /// * `data` - Additional data with no specified format. + #[allow(missing_docs)] + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /// Handles the receipt of multiple ERC-1155 token types. + /// This function is called at the end of a + /// [`IErc1155::safe_batch_transfer_from`] + /// after the balances have been updated. + /// + /// NOTE: To accept the transfer(s), + /// this must return [`BATCH_TRANSFER_FN_SELECTOR`], + /// or its own function selector. + /// + /// * `operator` - The address which initiated the batch transfer. + /// * `from` - The address which previously owned the token. + /// * `ids` - An array containing ids of each token being transferred + /// (order and length must match values array). + /// * `values` - An array containing amounts of each token + /// being transferred (order and length must match ids array). + /// * `data` - Additional data with no specified format. + #[allow(missing_docs)] + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); + } +} diff --git a/contracts/src/token/mod.rs b/contracts/src/token/mod.rs index 93385ec28..2bb3927b5 100644 --- a/contracts/src/token/mod.rs +++ b/contracts/src/token/mod.rs @@ -1,3 +1,4 @@ //! Token standards. +pub mod erc1155; pub mod erc20; pub mod erc721; diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 1bcc4ea42..0f92aca7c 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -6,6 +6,8 @@ *** xref:erc20.adoc#erc20-token-extensions[Extensions] ** xref:erc721.adoc[ERC-721] *** xref:erc721.adoc#erc721-token-extensions[Extensions] +** xref:erc1155.adoc[ERC-1155] +*** xref:erc1155.adoc#erc1155-token-extensions[Extensions] * xref:access-control.adoc[Access Control] * xref:crypto.adoc[Cryptography] diff --git a/docs/modules/ROOT/pages/ERC1155.adoc b/docs/modules/ROOT/pages/ERC1155.adoc new file mode 100644 index 000000000..d21721959 --- /dev/null +++ b/docs/modules/ROOT/pages/ERC1155.adoc @@ -0,0 +1,3 @@ += ERC-1155 + +ERC1155 is a novel token standard that aims to take the best from previous standards to create a xref:tokens.adoc#different-kinds-of-tokens[*fungibility-agnostic*] and *gas-efficient* xref:tokens.adoc#but_first_coffee_a_primer_on_token_contracts[token contract]. diff --git a/docs/modules/ROOT/pages/tokens.adoc b/docs/modules/ROOT/pages/tokens.adoc index b9c959384..97d48b376 100644 --- a/docs/modules/ROOT/pages/tokens.adoc +++ b/docs/modules/ROOT/pages/tokens.adoc @@ -24,7 +24,8 @@ In a nutshell, when dealing with non-fungibles (like your house) you care about Even though the concept of a token is simple, they have a variety of complexities in the implementation. Because everything in Ethereum is just a smart contract, and there are no rules about what smart contracts have to do, the community has developed a variety of *standards* (called EIPs or ERCs) for documenting how a contract can interoperate with other contracts. -You've probably heard of the ERC-20 or ERC-721 token standards, and that's why you're here. Head to our specialized guides to learn more about these: +You've probably heard of the ERC-20, ERC-721 or ERC-1155 token standards, and that's why you're here. Head to our specialized guides to learn more about these: * xref:erc20.adoc[ERC-20]: the most widespread token standard for fungible assets, albeit somewhat limited by its simplicity. * xref:erc721.adoc[ERC-721]: the de-facto solution for non-fungible tokens, often used for collectibles and games. + * xref:erc1155.adoc[ERC-1155]: a novel standard for multi-tokens, allowing for a single contract to represent multiple fungible and non-fungible tokens, along with batched operations for increased gas efficiency. diff --git a/examples/erc1155/Cargo.toml b/examples/erc1155/Cargo.toml new file mode 100644 index 000000000..1bb24e360 --- /dev/null +++ b/examples/erc1155/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "erc1155-example" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true + +[dependencies] +openzeppelin-stylus.workspace = true +alloy-primitives.workspace = true +stylus-sdk.workspace = true + +[dev-dependencies] +alloy.workspace = true +e2e.workspace = true +tokio.workspace = true +eyre.workspace = true +rand.workspace = true + +[features] +e2e = [] + +[lib] +crate-type = ["lib", "cdylib"] diff --git a/examples/erc1155/src/ERC1155ReceiverMock.sol b/examples/erc1155/src/ERC1155ReceiverMock.sol new file mode 100644 index 000000000..0ff6e73bc --- /dev/null +++ b/examples/erc1155/src/ERC1155ReceiverMock.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.21; + +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.1.0/contracts/token/ERC1155/IERC1155Receiver.sol"; +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.1.0/contracts/utils/introspection/ERC165.sol"; + +contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { + enum RevertType { + None, + RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, + Panic + } + + bytes4 private immutable _recRetval; + bytes4 private immutable _batRetval; + RevertType private immutable _error; + + event Received(address operator, address from, uint256 id, uint256 value, bytes data); + 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; + } +} diff --git a/examples/erc1155/src/constructor.sol b/examples/erc1155/src/constructor.sol new file mode 100644 index 000000000..f6452d95e --- /dev/null +++ b/examples/erc1155/src/constructor.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Erc1155Example { + mapping(address => mapping(uint256 => uint256)) private _balanceOf; + mapping(address => mapping(address => bool)) private _isApprovedForAll; +} diff --git a/examples/erc1155/src/lib.rs b/examples/erc1155/src/lib.rs new file mode 100644 index 000000000..d3fa59c6e --- /dev/null +++ b/examples/erc1155/src/lib.rs @@ -0,0 +1,45 @@ +#![cfg_attr(not(test), no_main)] +extern crate alloc; + +use alloc::vec::Vec; + +use alloy_primitives::{Address, U256}; +use openzeppelin_stylus::token::erc1155::Erc1155; +use stylus_sdk::{ + abi::Bytes, + prelude::{entrypoint, public, sol_storage}, +}; + +sol_storage! { + #[entrypoint] + struct Erc1155Example { + #[borrow] + Erc1155 erc1155; + } +} + +#[public] +#[inherit(Erc1155)] +impl Erc1155Example { + pub fn mint( + &mut self, + to: Address, + token_id: U256, + amount: U256, + data: Bytes, + ) -> Result<(), Vec> { + self.erc1155._mint(to, token_id, amount, &data)?; + Ok(()) + } + + pub fn mint_batch( + &mut self, + to: Address, + token_ids: Vec, + amounts: Vec, + data: Bytes, + ) -> Result<(), Vec> { + self.erc1155._mint_batch(to, token_ids, amounts, &data)?; + Ok(()) + } +} diff --git a/examples/erc1155/tests/abi/mod.rs b/examples/erc1155/tests/abi/mod.rs new file mode 100644 index 000000000..b7b13213d --- /dev/null +++ b/examples/erc1155/tests/abi/mod.rs @@ -0,0 +1,31 @@ +#![allow(dead_code)] +use alloy::sol; + +sol!( + #[sol(rpc)] + contract Erc1155 { + 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; + + 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 id); + + #[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/tests/erc1155.rs b/examples/erc1155/tests/erc1155.rs new file mode 100644 index 000000000..d693e56f6 --- /dev/null +++ b/examples/erc1155/tests/erc1155.rs @@ -0,0 +1,1570 @@ +#![cfg(feature = "e2e")] + +use abi::Erc1155; +use alloy::primitives::{fixed_bytes, uint, Address, U256}; +use e2e::{receipt, send, watch, Account, EventExt, ReceiptExt, Revert}; +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 { + (0..size).map(|_| U256::from(rand::random::())).collect() +} + +// ============================================================================ +// Integration Tests: ERC-1155 Token +// ============================================================================ + +#[e2e::test] +async fn constructs(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let _contract = Erc1155::new(contract_addr, &alice.wallet); + + Ok(()) +} + +#[e2e::test] +async fn invalid_array_length_error_in_balance_of_batch( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let token_ids = random_token_ids(3); + let accounts = vec![alice.address(), bob.address()]; + + let err = contract + .balanceOfBatch(accounts, token_ids) + .call() + .await + .expect_err("should return `ERC1155InvalidArrayLength`"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidArrayLength { + idsLength: uint!(3_U256), + valuesLength: uint!(2_U256) + })); + + Ok(()) +} + +#[e2e::test] +async fn balance_of_zero_balance(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + let token_ids = random_token_ids(1); + + let Erc1155::balanceOfReturn { balance } = + contract.balanceOf(alice.address(), token_ids[0]).call().await?; + assert_eq!(uint!(0_U256), 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 = Erc1155::new(contract_addr, &alice.wallet); + let accounts = + vec![alice.address(), bob.address(), dave.address(), charlie.address()]; + let token_ids = random_token_ids(4); + + let Erc1155::balanceOfBatchReturn { balances } = + contract.balanceOfBatch(accounts, token_ids).call().await?; + assert_eq!( + vec![uint!(0_U256), uint!(0_U256), uint!(0_U256), uint!(0_U256)], + balances + ); + + Ok(()) +} + +#[e2e::test] +async fn mints(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::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![0, 1, 2, 3].into() + ))?; + + assert!(receipt.emits(Erc1155::TransferSingle { + operator: alice_addr, + from: Address::ZERO, + to: alice_addr, + id: token_id, + value + })); + + let Erc1155::balanceOfReturn { balance } = + contract.balanceOf(alice_addr, token_id).call().await?; + assert_eq!(value, balance); + + Ok(()) +} + +#[e2e::test] +async fn mints_to_receiver_contract(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::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 Erc1155::balanceOfReturn { balance: initial_receiver_balance } = + contract.balanceOf(receiver_addr, token_id).call().await?; + + let receipt = + receipt!(contract.mint(receiver_addr, token_id, value, vec![].into()))?; + + assert!(receipt.emits(Erc1155::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: fixed_bytes!("").into(), + })); + + let Erc1155::balanceOfReturn { balance: receiver_balance } = + contract.balanceOf(receiver_addr, token_id).call().await?; + assert_eq!(initial_receiver_balance + value, receiver_balance); + + Ok(()) +} + +// FIXME: Update our `reverted_with` implementation such that we can also check +// when the error is a `stylus_sdk::call::Error`. +#[e2e::test] +#[ignore] +async fn errors_when_receiver_reverts_with_reason_in_mint( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = receiver::deploy( + &alice.wallet, + ERC1155ReceiverMock::RevertType::RevertWithMessage, + ) + .await?; + + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let _err = send!(contract.mint( + receiver_address, + token_id, + value, + vec![0, 1, 2, 3].into() + )) + .expect_err("should not mint when receiver errors with reason"); + + // assert!(err.reverted_with(stylus_sdk::call::Error::Revert( + // b"ERC1155ReceiverMock: reverting on receive".to_vec() + // ))); + Ok(()) +} + +#[e2e::test] +async fn errors_when_receiver_reverts_without_reason_in_mint( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = receiver::deploy( + &alice.wallet, + ERC1155ReceiverMock::RevertType::RevertWithoutMessage, + ) + .await?; + + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let err = send!(contract.mint( + receiver_address, + token_id, + value, + vec![0, 1, 2, 3].into() + )) + .expect_err("should not mint when receiver reverts"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidReceiver { + receiver: receiver_address + })); + + Ok(()) +} + +// FIXME: Update our `reverted_with` implementation such that we can also check +// when the error is a `stylus_sdk::call::Error`. +#[e2e::test] +#[ignore] +async fn errors_when_receiver_panics_in_mint( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = + receiver::deploy(&alice.wallet, ERC1155ReceiverMock::RevertType::Panic) + .await?; + + let alice_addr = alice.address(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let err = send!(contract.mint( + alice_addr, + token_id, + value, + vec![0, 1, 2, 3].into() + )) + .expect_err("should not mint when receiver panics"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidReceiver { + receiver: receiver_address + })); + + Ok(()) +} + +#[e2e::test] +async fn errors_when_invalid_receiver_contract_in_mint( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let _err = send!(contract.mint( + contract_addr, + token_id, + value, + vec![0, 1, 2, 3].into() + )) + .expect_err("should not mint when invalid receiver contract"); + + Ok(()) +} + +#[e2e::test] +async fn mint_batch( + alice: Account, + bob: Account, + dave: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let dave_addr = dave.address(); + let token_ids = random_token_ids(3); + let values = random_values(3); + + let accounts = vec![alice_addr, bob_addr, dave_addr]; + + for account in accounts { + let receipt = receipt!(contract.mintBatch( + account, + token_ids.clone(), + values.clone(), + vec![0, 1, 2, 3].into() + ))?; + + assert!(receipt.emits(Erc1155::TransferBatch { + operator: alice_addr, + from: Address::ZERO, + to: account, + ids: token_ids.clone(), + values: values.clone() + })); + + for (token_id, value) in token_ids.iter().zip(values.iter()) { + let Erc1155::balanceOfReturn { balance } = + contract.balanceOf(account, *token_id).call().await?; + assert_eq!(*value, balance); + } + + let Erc1155::balanceOfBatchReturn { balances } = contract + .balanceOfBatch(vec![account, account, account], token_ids.clone()) + .call() + .await?; + + assert_eq!(values, balances); + } + 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 = Erc1155::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 Erc1155::balanceOfBatchReturn { balances: initial_receiver_balances } = + contract + .balanceOfBatch( + vec![receiver_addr, receiver_addr], + token_ids.clone(), + ) + .call() + .await?; + + let receipt = receipt!(contract.mintBatch( + receiver_addr, + token_ids.clone(), + values.clone(), + vec![].into() + ))?; + + assert!(receipt.emits(Erc1155::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: fixed_bytes!("").into(), + })); + + let Erc1155::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_receiver_balances[idx] + value, + receiver_balances[idx] + ); + } + + Ok(()) +} + +// FIXME: Update our `reverted_with` implementation such that we can also check +// when the error is a `stylus_sdk::call::Error`. +#[e2e::test] +#[ignore] +async fn errors_when_receiver_reverts_with_reason_in_batch_mint( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = receiver::deploy( + &alice.wallet, + ERC1155ReceiverMock::RevertType::RevertWithMessage, + ) + .await?; + + let token_ids = random_token_ids(2); + let values = random_values(2); + + let _err = watch!(contract.mintBatch( + receiver_address, + token_ids.clone(), + values.clone(), + vec![].into() + )) + .expect_err("should not mint batch when receiver errors with reason"); + + // assert!(err.reverted_with(stylus_sdk::call::Error::Revert( + // b"ERC1155ReceiverMock: reverting on receive".to_vec() + // ))); + Ok(()) +} + +#[e2e::test] +async fn errors_when_receiver_reverts_without_reason_in_batch_mint( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = receiver::deploy( + &alice.wallet, + ERC1155ReceiverMock::RevertType::RevertWithoutMessage, + ) + .await?; + + let token_ids = random_token_ids(2); + let values = random_values(2); + + let err = send!(contract.mintBatch( + receiver_address, + token_ids.clone(), + values.clone(), + vec![].into() + )) + .expect_err("should not mint batch when receiver reverts"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidReceiver { + receiver: receiver_address + })); + + Ok(()) +} + +// FIXME: Update our `reverted_with` implementation such that we can also check +// when the error is a `stylus_sdk::call::Error`. +#[e2e::test] +#[ignore] +async fn errors_when_receiver_panics_in_batch_mint( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = + receiver::deploy(&alice.wallet, ERC1155ReceiverMock::RevertType::Panic) + .await?; + + let token_ids = random_token_ids(2); + let values = random_values(2); + + let err = send!(contract.mintBatch( + receiver_address, + token_ids.clone(), + values.clone(), + vec![].into() + )) + .expect_err("should not mint batch when receiver panics"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidReceiver { + receiver: receiver_address + })); + + Ok(()) +} + +#[e2e::test] +async fn errors_when_invalid_receiver_contract_in_batch_mint( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let token_ids = random_token_ids(2); + let values = random_values(2); + + let _err = send!(contract.mintBatch( + contract_addr, + token_ids.clone(), + values.clone(), + vec![].into() + )) + .expect_err("should not mint batch when invalid receiver contract"); + + Ok(()) +} + +#[e2e::test] +async fn error_invalid_array_length_in_batch_mint( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract_alice = Erc1155::new(contract_addr, &alice.wallet); + + let bob_addr = bob.address(); + let token_ids = random_token_ids(2); + let values = random_values(2); + + let err = send!(contract_alice.mintBatch( + bob_addr, + vec![token_ids[0]], + values, + vec![].into() + )) + .expect_err("should return `ERC1155InvalidArrayLength`"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidArrayLength { + idsLength: uint!(1_U256), + valuesLength: uint!(2_U256) + })); + + 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 = Erc1155::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(Erc1155::ApprovalForAll { + account: alice_addr, + operator: bob_addr, + approved: approved_value, + })); + + let Erc1155::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(Erc1155::ApprovalForAll { + account: alice_addr, + operator: bob_addr, + approved: approved_value, + })); + + let Erc1155::isApprovedForAllReturn { approved } = + contract.isApprovedForAll(alice_addr, bob_addr).call().await?; + assert_eq!(approved_value, approved); + + Ok(()) +} + +#[e2e::test] +async fn error_when_invalid_operator_approval_for_all( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let invalid_operator = Address::ZERO; + + let err = send!(contract.setApprovalForAll(invalid_operator, true)) + .expect_err("should return `ERC1155InvalidOperator`"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidOperator { + operator: invalid_operator + })); + + 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 = Erc1155::new(contract_addr, &alice.wallet); + + let invalid_operator = Address::ZERO; + + let Erc1155::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 = Erc1155::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 Erc1155::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr, token_id).call().await?; + let Erc1155::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(Erc1155::TransferSingle { + operator: alice_addr, + from: alice_addr, + to: bob_addr, + id: token_id, + value + })); + + let Erc1155::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr, token_id).call().await?; + assert_eq!(initial_alice_balance - value, alice_balance); + + let Erc1155::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 = Erc1155::new(contract_addr, &alice.wallet); + let contract_bob = Erc1155::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 Erc1155::balanceOfReturn { balance: initial_alice_balance } = + contract_alice.balanceOf(alice_addr, token_id).call().await?; + let Erc1155::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(Erc1155::TransferSingle { + operator: alice_addr, + from: bob_addr, + to: alice_addr, + id: token_id, + value + })); + + let Erc1155::balanceOfReturn { balance: alice_balance } = + contract_alice.balanceOf(alice_addr, token_id).call().await?; + assert_eq!(initial_alice_balance + value, alice_balance); + + let Erc1155::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 = Erc1155::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 Erc1155::balanceOfReturn { balance: initial_alice_balance } = + contract.balanceOf(alice_addr, token_id).call().await?; + let Erc1155::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(Erc1155::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: fixed_bytes!("").into(), + })); + + let Erc1155::balanceOfReturn { balance: alice_balance } = + contract.balanceOf(alice_addr, token_id).call().await?; + assert_eq!(initial_alice_balance - value, alice_balance); + + let Erc1155::balanceOfReturn { balance: receiver_balance } = + contract.balanceOf(receiver_addr, token_id).call().await?; + assert_eq!(initial_receiver_balance + value, receiver_balance); + + Ok(()) +} + +// FIXME: Update our `reverted_with` implementation such that we can also check +// when the error is a `stylus_sdk::call::Error`. +#[e2e::test] +#[ignore] +async fn errors_when_receiver_reverts_with_reason( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = receiver::deploy( + &alice.wallet, + ERC1155ReceiverMock::RevertType::RevertWithMessage, + ) + .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 _err = send!(contract.safeTransferFrom( + alice_addr, + receiver_address, + token_id, + value, + vec![].into() + )) + .expect_err("should not transfer when receiver errors with reason"); + + // assert!(err.reverted_with(stylus_sdk::call::Error::Revert( + // b"ERC1155ReceiverMock: reverting on receive".to_vec() + // ))); + Ok(()) +} + +#[e2e::test] +async fn errors_when_receiver_reverts_without_reason( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = receiver::deploy( + &alice.wallet, + ERC1155ReceiverMock::RevertType::RevertWithoutMessage, + ) + .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 err = send!(contract.safeTransferFrom( + alice_addr, + receiver_address, + token_id, + value, + vec![].into() + )) + .expect_err("should not transfer when receiver reverts"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidReceiver { + receiver: receiver_address + })); + + Ok(()) +} + +// FIXME: Update our `reverted_with` implementation such that we can also check +// when the error is a `stylus_sdk::call::Error`. +#[e2e::test] +#[ignore] +async fn errors_when_receiver_panics(alice: Account) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = + receiver::deploy(&alice.wallet, ERC1155ReceiverMock::RevertType::Panic) + .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 err = send!(contract.safeTransferFrom( + alice_addr, + receiver_address, + token_id, + value, + vec![].into() + )) + .expect_err("should not transfer when receiver panics"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidReceiver { + receiver: receiver_address + })); + + Ok(()) +} + +#[e2e::test] +async fn errors_when_invalid_receiver_contract( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::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![0, 1, 2, 3].into() + ))?; + + let _err = send!(contract.safeTransferFrom( + alice_addr, + contract_addr, + token_id, + value, + vec![].into() + )) + .expect_err("should not transfer when invalid receiver contract"); + + Ok(()) +} + +#[e2e::test] +async fn error_when_invalid_receiver_safe_transfer_from( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let invalid_receiver = Address::ZERO; + 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 err = send!(contract.safeTransferFrom( + alice_addr, + invalid_receiver, + token_id, + value, + vec![].into() + )) + .expect_err("should return `ERC1155InvalidReceiver`"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidReceiver { + receiver: invalid_receiver + })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_missing_approval_safe_transfer_from( + alice: Account, + bob: Account, + dave: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let dave_addr = dave.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 err = send!(contract.safeTransferFrom( + bob_addr, + dave_addr, + token_id, + value, + vec![].into() + )) + .expect_err("should return `ERC1155MissingApprovalForAll`"); + + assert!(err.reverted_with(Erc1155::ERC1155MissingApprovalForAll { + operator: alice_addr, + owner: bob_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_insufficient_balance_safe_transfer_from( + alice: Account, + bob: Account, + dave: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract_alice = Erc1155::new(contract_addr, &alice.wallet); + let contract_bob = Erc1155::new(contract_addr, &bob.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let dave_addr = dave.address(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let _ = + watch!(contract_alice.mint(bob_addr, token_id, value, vec![].into())); + let _ = watch!(contract_bob.setApprovalForAll(alice_addr, true)); + + let err = send!(contract_alice.safeTransferFrom( + bob_addr, + dave_addr, + token_id, + value + uint!(1_U256), + vec![].into() + )) + .expect_err("should return `ERC1155InsufficientBalance`"); + + assert!(err.reverted_with(Erc1155::ERC1155InsufficientBalance { + sender: bob_addr, + balance: value, + needed: value + uint!(1_U256), + id: token_id + })); + + 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 = Erc1155::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 Erc1155::balanceOfBatchReturn { balances: initial_alice_balances } = + contract_alice + .balanceOfBatch(vec![alice_addr, alice_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155::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(Erc1155::TransferBatch { + operator: alice_addr, + from: alice_addr, + to: bob_addr, + ids: token_ids.clone(), + values: values.clone() + })); + + let Erc1155::balanceOfBatchReturn { balances: alice_balances } = + contract_alice + .balanceOfBatch(vec![alice_addr, alice_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155::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 = Erc1155::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 Erc1155::balanceOfBatchReturn { balances: initial_alice_balances } = + contract + .balanceOfBatch(vec![alice_addr, alice_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155::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(Erc1155::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: fixed_bytes!("").into(), + })); + + let Erc1155::balanceOfBatchReturn { balances: alice_balances } = contract + .balanceOfBatch(vec![alice_addr, alice_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155::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(()) +} + +// FIXME: Update our `reverted_with` implementation such that we can also check +// when the error is a `stylus_sdk::call::Error`. +#[e2e::test] +#[ignore] +async fn errors_when_receiver_reverts_with_reason_in_batch_transfer( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = receiver::deploy( + &alice.wallet, + ERC1155ReceiverMock::RevertType::RevertWithMessage, + ) + .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 _err = send!(contract.safeBatchTransferFrom( + alice_addr, + receiver_address, + token_ids, + values, + vec![].into() + )) + .expect_err("should not transfer when receiver errors with reason"); + + // assert!(err.reverted_with(stylus_sdk::call::Error::Revert( + // b"ERC1155ReceiverMock: reverting on receive".to_vec() + // ))); + Ok(()) +} + +#[e2e::test] +async fn errors_when_receiver_reverts_without_reason_in_batch_transfer( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = receiver::deploy( + &alice.wallet, + ERC1155ReceiverMock::RevertType::RevertWithoutMessage, + ) + .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 err = send!(contract.safeBatchTransferFrom( + alice_addr, + receiver_address, + token_ids, + values, + vec![].into() + )) + .expect_err("should not transfer when receiver reverts"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidReceiver { + receiver: receiver_address + })); + + Ok(()) +} + +// FIXME: Update our `reverted_with` implementation such that we can also check +// when the error is a `stylus_sdk::call::Error`. +#[e2e::test] +#[ignore] +async fn errors_when_receiver_panics_in_batch_transfer( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let receiver_address = + receiver::deploy(&alice.wallet, ERC1155ReceiverMock::RevertType::Panic) + .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 err = send!(contract.safeBatchTransferFrom( + alice_addr, + receiver_address, + token_ids, + values, + vec![].into() + )) + .expect_err("should not transfer when receiver panics"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidReceiver { + receiver: receiver_address + })); + + Ok(()) +} + +#[e2e::test] +async fn errors_when_invalid_receiver_contract_in_batch_transfer( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + 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 _err = send!(contract.safeBatchTransferFrom( + alice_addr, + contract_addr, + token_ids, + values, + vec![].into() + )) + .expect_err("should not transfer when invalid receiver contract"); + + 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 = Erc1155::new(contract_addr, &alice.wallet); + let contract_bob = Erc1155::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 Erc1155::balanceOfBatchReturn { balances: initial_dave_balances } = + contract_alice + .balanceOfBatch(vec![dave_addr, dave_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155::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(Erc1155::TransferBatch { + operator: alice_addr, + from: bob_addr, + to: dave_addr, + ids: token_ids.clone(), + values: values.clone() + })); + + let Erc1155::balanceOfBatchReturn { balances: bob_balances } = + contract_alice + .balanceOfBatch(vec![bob_addr, bob_addr], token_ids.clone()) + .call() + .await?; + + let Erc1155::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(()) +} + +#[e2e::test] +async fn error_when_invalid_receiver_safe_batch_transfer_from( + alice: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let invalid_receiver = Address::ZERO; + 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 err = send!(contract.safeBatchTransferFrom( + alice_addr, + invalid_receiver, + token_ids.clone(), + values.clone(), + vec![].into() + )) + .expect_err("should return `ERC1155InvalidReceiver`"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidReceiver { + receiver: invalid_receiver + })); + + Ok(()) +} + +#[e2e::test] +async fn error_invalid_array_length_in_safe_batch_transfer_from( + alice: Account, + bob: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract_alice = Erc1155::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 err = send!(contract_alice.safeBatchTransferFrom( + alice_addr, + bob_addr, + vec![token_ids[0]], + values.clone(), + vec![].into() + )) + .expect_err("should return `ERC1155InvalidArrayLength`"); + + assert!(err.reverted_with(Erc1155::ERC1155InvalidArrayLength { + idsLength: uint!(1_U256), + valuesLength: uint!(2_U256) + })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_missing_approval_safe_batch_transfer_from( + alice: Account, + bob: Account, + dave: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract = Erc1155::new(contract_addr, &alice.wallet); + + let alice_addr = alice.address(); + let bob_addr = bob.address(); + let dave_addr = dave.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 err = send!(contract.safeBatchTransferFrom( + bob_addr, + dave_addr, + token_ids.clone(), + values.clone(), + vec![].into() + )) + .expect_err("should return `ERC1155MissingApprovalForAll`"); + + assert!(err.reverted_with(Erc1155::ERC1155MissingApprovalForAll { + operator: alice_addr, + owner: bob_addr + })); + + Ok(()) +} + +#[e2e::test] +async fn error_when_insufficient_balance_safe_batch_transfer_from( + alice: Account, + bob: Account, + dave: Account, +) -> eyre::Result<()> { + let contract_addr = alice.as_deployer().deploy().await?.address()?; + let contract_alice = Erc1155::new(contract_addr, &alice.wallet); + let contract_bob = Erc1155::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 err = send!(contract_alice.safeBatchTransferFrom( + bob_addr, + dave_addr, + token_ids.clone(), + vec![values[0] + uint!(1_U256), values[1]], + vec![].into() + )) + .expect_err("should return `ERC1155InsufficientBalance`"); + + assert!(err.reverted_with(Erc1155::ERC1155InsufficientBalance { + sender: bob_addr, + balance: values[0], + needed: values[0] + uint!(1_U256), + id: token_ids[0] + })); + + Ok(()) +} diff --git a/examples/erc1155/tests/mock/mod.rs b/examples/erc1155/tests/mock/mod.rs new file mode 100644 index 000000000..4c0db7eaa --- /dev/null +++ b/examples/erc1155/tests/mock/mod.rs @@ -0,0 +1 @@ +pub mod receiver; diff --git a/examples/erc1155/tests/mock/receiver.rs b/examples/erc1155/tests/mock/receiver.rs new file mode 100644 index 000000000..da021c4cb --- /dev/null +++ b/examples/erc1155/tests/mock/receiver.rs @@ -0,0 +1,97 @@ +#![allow(dead_code)] +#![cfg(feature = "e2e")] +use alloy::{ + primitives::{fixed_bytes, Address, FixedBytes}, + sol, +}; +use e2e::Wallet; + +const REC_RETVAL: FixedBytes<4> = fixed_bytes!("f23a6e61"); +const BAT_RETVAL: FixedBytes<4> = fixed_bytes!("bc197c81"); + +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()) +}