diff --git a/contracts/src/token/erc1155/extensions/supply.rs b/contracts/src/token/erc1155/extensions/supply.rs index 3f7a68e2..ea6aad9a 100644 --- a/contracts/src/token/erc1155/extensions/supply.rs +++ b/contracts/src/token/erc1155/extensions/supply.rs @@ -10,3 +10,190 @@ //! //! CAUTION: This extension should not be added in an upgrade to an already //! deployed contract. + +use alloy_primitives::{uint, Address, U256}; +use stylus_proc::sol_storage; +use stylus_sdk::prelude::*; + +use crate::{ + token::erc1155::{Erc1155, Error}, + utils::math::storage::SubAssignUnchecked, +}; + +sol_storage! { + /// State of [`crate::token::erc1155::Erc1155`] token's supply. + pub struct Erc1155Supply { + /// Erc1155 contract storage. + Erc1155 erc1155; + /// Mapping from token ID to total supply. + mapping(uint256 => uint256) _total_supply; + /// Total supply of all token IDs. + uint256 _total_supply_all; + } +} + +#[public] +impl Erc1155Supply { + fn total_supply(&self, token_id: U256) -> U256 { + self._total_supply.get(token_id) + } + + fn total_supply_all(&self) -> U256 { + *self._total_supply_all + } + + fn exists(&self, token_id: U256) -> bool { + self.total_supply(token_id) > uint!(0_U256) + } +} + +impl Erc1155Supply { + /// Override of [`Erc1155::_update`] that restricts normal minting to after + /// construction. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account of the sender. + /// * `to` - Account of the recipient. + /// * `token_ids` - Array of all token id. + /// * `values` - Array of all amount of tokens to be supplied. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event if the arrays contain one element, and + /// [`TransferBatch`] otherwise. + fn _update( + &mut self, + from: Address, + to: Address, + token_ids: Vec, + values: Vec, + ) -> Result<(), Error> { + self.erc1155._update(from, to, token_ids.clone(), values.clone())?; + + if from.is_zero() { + let mut total_mint_value = uint!(0_U256); + token_ids.iter().zip(values.iter()).for_each( + |(&token_id, &value)| { + let total_supply = + self.total_supply(token_id).checked_add(value).expect( + "should not exceed `U256::MAX` for `_total_supply`", + ); + self._total_supply.setter(token_id).set(total_supply); + total_mint_value += value; + }, + ); + let total_supply_all = + self.total_supply_all().checked_add(total_mint_value).expect( + "should not exceed `U256::MAX` for `_total_supply_all`", + ); + self._total_supply_all.set(total_supply_all); + } + + if to.is_zero() { + let mut total_burn_value = uint!(0_U256); + token_ids.iter().zip(values.iter()).for_each( + |(&token_id, &value)| { + self._total_supply + .setter(token_id) + .sub_assign_unchecked(value); + total_burn_value += value; + }, + ); + let total_supply_all = + self._total_supply_all.get() - total_burn_value; + self._total_supply_all.set(total_supply_all); + } + Ok(()) + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use alloy_primitives::{address, Address, U256}; + + use super::Erc1155Supply; + use crate::token::erc1155::IErc1155; + + const ALICE: Address = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); + const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526"); + + pub(crate) fn random_token_ids(size: usize) -> Vec { + (0..size).map(|_| U256::from(rand::random::())).collect() + } + + pub(crate) fn random_values(size: usize) -> Vec { + (0..size).map(|_| U256::from(rand::random::())).collect() + } + + fn init( + contract: &mut Erc1155Supply, + reciever: Address, + size: usize, + ) -> (Vec, Vec) { + let token_ids = random_token_ids(size); + let values = random_values(size); + + contract + ._update(Address::ZERO, reciever, token_ids.clone(), values.clone()) + .expect("Supply failed"); + (token_ids, values) + } + + #[motsu::test] + fn test_supply_of_zero_supply(contract: Erc1155Supply) { + let token_ids = random_token_ids(1); + assert_eq!(U256::ZERO, contract.total_supply(token_ids[0])); + assert_eq!(U256::ZERO, contract.total_supply_all()); + assert!(!contract.exists(token_ids[0])); + } + + #[motsu::test] + fn test_supply_with_zero_address_sender(contract: Erc1155Supply) { + let token_ids = random_token_ids(1); + let values = random_values(1); + contract + ._update(Address::ZERO, ALICE, token_ids.clone(), values.clone()) + .expect("Supply failed"); + assert_eq!(values[0], contract.total_supply(token_ids[0])); + assert_eq!(values[0], contract.total_supply_all()); + assert!(contract.exists(token_ids[0])); + } + + #[motsu::test] + fn test_supply_with_zero_address_receiver(contract: Erc1155Supply) { + let (token_ids, values) = init(contract, ALICE, 1); + contract + ._update(ALICE, Address::ZERO, token_ids.clone(), values.clone()) + .expect("Supply failed"); + assert_eq!(U256::ZERO, contract.total_supply(token_ids[0])); + assert_eq!(U256::ZERO, contract.total_supply_all()); + assert!(!contract.exists(token_ids[0])); + } + + #[motsu::test] + fn test_supply_batch(contract: Erc1155Supply) { + let (token_ids, values) = init(contract, BOB, 4); + assert_eq!( + values[0], + contract.erc1155.balance_of(BOB, token_ids[0]).unwrap() + ); + assert_eq!( + values[1], + contract.erc1155.balance_of(BOB, token_ids[1]).unwrap() + ); + assert_eq!( + values[2], + contract.erc1155.balance_of(BOB, token_ids[2]).unwrap() + ); + assert_eq!( + values[3], + contract.erc1155.balance_of(BOB, token_ids[3]).unwrap() + ); + assert!(contract.exists(token_ids[0])); + assert!(contract.exists(token_ids[1])); + assert!(contract.exists(token_ids[2])); + assert!(contract.exists(token_ids[3])); + } +} diff --git a/contracts/src/token/erc1155/extensions/uri_storage.rs b/contracts/src/token/erc1155/extensions/uri_storage.rs index 9a4f04fa..f30a183d 100644 --- a/contracts/src/token/erc1155/extensions/uri_storage.rs +++ b/contracts/src/token/erc1155/extensions/uri_storage.rs @@ -5,7 +5,7 @@ use alloc::string::String; use alloy_primitives::U256; use alloy_sol_types::sol; -use stylus_proc::{external, sol_storage}; +use stylus_proc::{public, sol_storage}; use stylus_sdk::evm; sol! { @@ -42,7 +42,7 @@ impl Erc1155UriStorage { } } -#[external] +#[public] impl Erc1155UriStorage { /// Returns the Uniform Resource Identifier (URI) for `token_id` token. /// diff --git a/contracts/src/token/erc1155/mod.rs b/contracts/src/token/erc1155/mod.rs index d5a06302..ba8c35c0 100644 --- a/contracts/src/token/erc1155/mod.rs +++ b/contracts/src/token/erc1155/mod.rs @@ -422,6 +422,13 @@ impl IErc1155 for Erc1155 { value: U256, data: Bytes, ) -> Result<(), Self::Error> { + let sender = msg::sender(); + if from != sender && !self.is_approved_for_all(from, sender) { + return Err(Error::MissingApprovalForAll( + ERC1155MissingApprovalForAll { operator: sender, owner: from }, + )); + } + self._safe_transfer_from(from, to, token_id, value, data)?; Ok(()) } @@ -433,6 +440,13 @@ impl IErc1155 for Erc1155 { values: Vec, data: Bytes, ) -> Result<(), Self::Error> { + let sender = msg::sender(); + if from != sender && !self.is_approved_for_all(from, sender) { + return Err(Error::MissingApprovalForAll( + ERC1155MissingApprovalForAll { operator: sender, owner: from }, + )); + } + self._safe_batch_transfer_from(from, to, token_ids, values, data)?; Ok(()) } } @@ -479,28 +493,33 @@ impl Erc1155 { let operator = msg::sender(); for (&token_id, &value) in token_ids.iter().zip(values.iter()) { - let from_balance = self._balances.get(token_id).get(from); - if from_balance < value { - return Err(Error::InsufficientBalance( - ERC1155InsufficientBalance { - sender: from, - balance: from_balance, - needed: value, - token_id, - }, - )); + if !from.is_zero() { + // let from_balance = self._balances.get(token_id).get(from); + 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); } - self._balances - .setter(token_id) - .setter(from) - .sub_assign_unchecked(value); if !to.is_zero() { - self._balances + let new_value = self + ._balances .setter(token_id) .setter(to) .checked_add(value) .expect("should not exceed `U256::MAX` for `_balances`"); + self._balances.setter(token_id).setter(to).set(new_value); } } @@ -563,6 +582,189 @@ impl Erc1155 { Ok(()) } + /// Transfers a `value` tokens of token type `token_id` from `from` to `to`. + /// + /// Requirements: + /// + /// - `to` cannot be the zero address. + /// - `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 magic value. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account of the sender. + /// * `to` - Account of the recipient. + /// * `token_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 the zero address, then the error [`Error::InvalidReceiver`] + /// is returned. + /// If `from` is the zero address, then the error + /// [`Error::InvalidSender`] is returned. + /// + /// Event + /// + /// Emits a [`TransferSingle`] event. + fn _safe_transfer_from( + &mut self, + from: Address, + to: Address, + token_id: U256, + value: U256, + 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, + vec![token_id], + vec![value], + data, + ) + } + + /// Refer to: + /// https://docs.openzeppelin.com/contracts/5.x/api/token/erc1155#ERC1155-_safeBatchTransferFrom-address-address-uint256---uint256---bytes- + /// [Batched](https://docs.openzeppelin.com/contracts/5.x/erc1155#batch-operations) + /// version of [`Self::_safe_transfer_from`]. + /// + /// Requirements: + /// + /// - If `to` refers to a smart contract, it must implement + /// {IERC1155Receiver-onERC1155BatchReceived} and return the + /// acceptance magic value. + /// - `ids` and `values` must have the same length. + /// + /// # Arguments + /// + /// * `&mut self` - Write access to the contract's state. + /// * `from` - Account of the sender. + /// * `to` - Account of the recipient. + /// * `token_ids` - Array of all token id. + /// * `values` - Array of all amount of tokens to be transferred. + /// * `data` - Additional data with no specified format, sent in call to + /// `to`. + /// + /// # Errors + /// + /// If `to` is the zero address, then the error [`Error::InvalidReceiver`] + /// is returned. + /// If `from` is the zero address, then the error + /// [`Error::InvalidSender`] is returned. + /// + /// Event + /// + /// Emits a [`TransferBatch`] event. + fn _safe_batch_transfer_from( + &mut self, + from: Address, + to: Address, + token_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, token_ids, values, data) + } + + /// Creates a `value` amount of tokens of type `token_id`, and assigns + /// them to `to`. + /// + /// Requirements: + /// + /// - `to` cannot be the zero address. + /// - If `to` refers to a smart contract, it must implement + /// [`IERC1155Receiver::on_erc_1155_received`] and return the + /// acceptance magic value. + /// + /// # Events + /// + /// Emits a [`TransferSingle`] event. + fn _mint( + &mut self, + to: Address, + token_id: U256, + value: U256, + data: Bytes, + ) -> Result<(), Error> { + if to.is_zero() { + return Err(Error::InvalidReceiver(ERC1155InvalidReceiver { + receiver: to, + })); + } + self._update_with_acceptance_check( + Address::ZERO, + to, + vec![token_id], + vec![value], + data, + )?; + Ok(()) + } + + /// Refer to: + /// https://docs.openzeppelin.com/contracts/5.x/api/token/erc1155#ERC1155-_mintBatch-address-uint256---uint256---bytes- + /// [Batched](https://docs.openzeppelin.com/contracts/5.x/erc1155#batch-operations) + /// version of [`Self::_mint`]. + /// + /// Requirements: + /// + /// - `to` cannot be the zero address. + /// - If `to` refers to a smart contract, it must implement + /// [`IERC1155Receiver::on_erc_1155_received`] and return the + /// acceptance magic value. + /// + /// # Events + /// + /// Emits a [`TransferBatch`] event. + fn _mint_batch( + &mut self, + to: Address, + token_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, + token_ids, + values, + data, + )?; + Ok(()) + } + /// Approve `operator` to operate on all of `owner` tokens /// /// Emits an [`ApprovalForAll`] event. @@ -743,6 +945,7 @@ impl Erc1155 { #[cfg(all(test, feature = "std"))] mod tests { use alloy_primitives::{address, uint, Address, U256}; + use alloy_sol_types::abi::token; use stylus_sdk::{contract, msg}; use super::{ @@ -760,6 +963,10 @@ mod tests { (0..size).map(|_| U256::from(rand::random::())).collect() } + pub(crate) fn random_values(size: usize) -> Vec { + (0..size).map(|_| U256::from(rand::random::())).collect() + } + #[motsu::test] fn test_balance_of_zero_balance(contract: Erc1155) { let owner = msg::sender(); @@ -834,4 +1041,41 @@ mod tests { }) if operator == invalid_operator )); } + + #[motsu::test] + fn test_mints(contract: Erc1155) { + let alice = msg::sender(); + let token_id = random_token_ids(1)[0]; + let value = random_values(1)[0]; + + let initial_balance = contract + .balance_of(alice, token_id) + .expect("should return the balance of Alice"); + + 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) + .expect("should return the balance of Alice"); + + assert_eq!(balance, initial_balance + value); + } + + #[motsu::test] + fn test_safe_transfer_from(contract: Erc1155) { + // let alice = msg::sender(); + // let token_id = U256::from(1); + // let value = U256::from(10); + + // contract + // .safe_transfer_from(alice, BOB, token_id, value, vec![]) + // .expect("should transfer tokens from Alice to Bob"); + + // let balance = contract + // .balance_of(BOB, token_id) + // .expect("should return Bob's balance of the token"); + // assert_eq!(value, balance); + } }