Repository: OpenZeppelin/rust-contracts-stylus
Pull request #422 (2024-11-27 02:34:51)

build(deps): bump crate-ci/typos from 1.28.0 to 1.28.1
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.28.0 to 1.28.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/crate-ci/typos/releases">crate-ci/typos's releases</a>.</em></p>
<blockquote>
<h2>v1.28.1</h2>
<h2>[1.28.1] - 2024-11-26</h2>
<h3>Fixes</h3>
<ul>
<li>Add back in <code>lock</code> file types accidentally removed in 1.28 (<code>go.sum</code>, <code>requirements.txt</code>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/crate-ci/typos/blob/master/CHANGELOG.md">crate-ci/typos's changelog</a>.</em></p>
<blockquote>
<h2>[1.28.1] - 2024-11-26</h2>
<h3>Fixes</h3>
<ul>
<li>Add back in <code>lock</code> file types accidentally removed in 1.28 (<code>go.sum</code>, <code>requirements.txt</code>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/crate-ci/typos/commit/bd36f89fcd3424dcefd442894589e6ee572a59f2"><code>bd36f89</code></a> chore: Release</li>
<li><a href="https://github.com/crate-ci/typos/commit/9b3917ceee2adf7638bb27defd8539bab4b801d9"><code>9b3917c</code></a> docs: Update changelog</li>
<li><a href="https://github.com/crate-ci/typos/commit/8a0dae679343edcebad73d9d10a8dc33a9f9c0e8"><code>8a0dae6</code></a> Merge pull request <a href="https://redirect.github.com/crate-ci/typos/issues/1159">#1159</a> from epage/go</li>
<li><a href="https://github.com/crate-ci/typos/commit/70f236086fee4675c2b4a67a50771e08181cddd9"><code>70f2360</code></a> fix: Re-add go.sum, requirements.txt to 'lock' file type</li>
<li>See full diff in <a href="https://github.com/crate-ci/typos/compare/v1.28.0...v1.28.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.28.0&new-version=1.28.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *contracts-stylus* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | ad4edcb5c72e15ef01296192974f095d63c3a572 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/contracts-stylus/deploys/6746854da166170008b7e761 |

Patch:
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 75cf763..71c35ac 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -127,4 +127,4 @@ jobs:
         uses: actions/checkout@v4
 
       - name: Check spelling of files in the workspace
-        uses: crate-ci/typos@v1.28.0
+        uses: crate-ci/typos@v1.28.1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65f2e55..c6e1c31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [Unreleased]
+
+### Added
+
+- `IErc1155Burnable` extension. #417 
+
+### Changed
+
+- Use `function_selector!` to calculate transfer type selector in `Erc1155`. #417 
+
+### Fixed
+
+-
+
 ## [v0.2.0-alpha.1] - 2024-11-15
 
 ### Added
diff --git a/Cargo.lock b/Cargo.lock
index 953b3db..10b0e2b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -819,6 +819,7 @@ dependencies = [
  "e2e",
  "eyre",
  "futures",
+ "itertools 0.13.0",
  "keccak-const",
  "koba",
  "openzeppelin-stylus",
@@ -1556,7 +1557,6 @@ dependencies = [
  "e2e",
  "eyre",
  "openzeppelin-stylus",
- "rand",
  "stylus-sdk",
  "tokio",
 ]
diff --git a/benches/Cargo.toml b/benches/Cargo.toml
index 5cb17d8..9e46f7a 100644
--- a/benches/Cargo.toml
+++ b/benches/Cargo.toml
@@ -17,3 +17,4 @@ koba.workspace = true
 e2e.workspace = true
 serde = "1.0.203"
 keccak-const = "0.2.0"
+itertools = "0.13.0"
diff --git a/benches/src/erc1155.rs b/benches/src/erc1155.rs
index 4806a11..e3df0d6 100644
--- a/benches/src/erc1155.rs
+++ b/benches/src/erc1155.rs
@@ -24,6 +24,8 @@ sol!(
         function safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data) external;
         function mint(address to, uint256 id, uint256 amount, bytes memory data) external;
         function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) external;
+        function burn(address account, uint256 id, uint256 value) external;
+        function burnBatch(address account, uint256[] memory ids, uint256[] memory values) external;
     }
 );
 
@@ -56,10 +58,16 @@ pub async fn run_with(
 
     let bob = Account::new().await?;
     let bob_addr = bob.address();
+    let bob_wallet = ProviderBuilder::new()
+        .network::<AnyNetwork>()
+        .with_recommended_fillers()
+        .wallet(EthereumWallet::from(bob.signer.clone()))
+        .on_http(bob.url().parse()?);
 
     let contract_addr = deploy(&alice, cache_opt).await?;
 
     let contract = Erc1155::new(contract_addr, &alice_wallet);
+    let contract_bob = Erc1155::new(contract_addr, &bob_wallet);
 
     let token_1 = uint!(1_U256);
     let token_2 = uint!(2_U256);
@@ -87,7 +95,10 @@ pub async fn run_with(
         (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()))?)
+        (safeBatchTransferFromCall::SIGNATURE, receipt!(contract.safeBatchTransferFrom(alice_addr, bob_addr, ids.clone(), values.clone(), data))?),
+        // We should burn Bob's tokens on behalf of Bob not Alice.
+        (burnCall::SIGNATURE, receipt!(contract_bob.burn(bob_addr, token_1, value_1))?),
+        (burnBatchCall::SIGNATURE, receipt!(contract_bob.burnBatch(bob_addr, ids, values))?),
     ];
 
     receipts
diff --git a/benches/src/main.rs b/benches/src/main.rs
index 94fbaa3..2ec844a 100644
--- a/benches/src/main.rs
+++ b/benches/src/main.rs
@@ -1,21 +1,31 @@
 use benches::{
-    access_control, erc20, erc721, merkle_proofs, ownable,
+    access_control, erc1155, erc20, erc721, merkle_proofs, ownable,
     report::BenchmarkReport,
 };
 use futures::FutureExt;
+use itertools::Itertools;
 
 #[tokio::main]
 async fn main() -> eyre::Result<()> {
-    let report = futures::future::try_join_all([
+    let benchmarks = [
         access_control::bench().boxed(),
         erc20::bench().boxed(),
         erc721::bench().boxed(),
         merkle_proofs::bench().boxed(),
         ownable::bench().boxed(),
-    ])
-    .await?
-    .into_iter()
-    .fold(BenchmarkReport::default(), BenchmarkReport::merge_with);
+        erc1155::bench().boxed(),
+    ];
+
+    // Run benchmarks max 3 at the same time.
+    // Otherwise, nitro test node can overload and revert transaction.
+    const MAX_PARALLEL: usize = 3;
+    let mut report = BenchmarkReport::default();
+    for chunk in &benchmarks.into_iter().chunks(MAX_PARALLEL) {
+        report = futures::future::try_join_all(chunk)
+            .await?
+            .into_iter()
+            .fold(report, BenchmarkReport::merge_with);
+    }
 
     println!();
     println!("{report}");
diff --git a/contracts/src/token/erc1155/extensions/burnable.rs b/contracts/src/token/erc1155/extensions/burnable.rs
new file mode 100644
index 0000000..bae3789
--- /dev/null
+++ b/contracts/src/token/erc1155/extensions/burnable.rs
@@ -0,0 +1,351 @@
+//! Optional Burnable extension of the ERC-1155 standard.
+
+use alloc::vec::Vec;
+
+use alloy_primitives::{Address, U256};
+use stylus_sdk::msg;
+
+use crate::token::erc1155::{
+    ERC1155MissingApprovalForAll, Erc1155, Error, IErc1155,
+};
+
+/// Extension of [`Erc1155`] that allows token holders to destroy both their
+/// own tokens and those that they have been approved to use.
+pub trait IErc1155Burnable {
+    /// The error type associated to this trait implementation.
+    type Error: Into<alloc::vec::Vec<u8>>;
+
+    /// Destroys a `value` amount of token from `account`.
+    ///
+    /// # Arguments
+    ///
+    /// * `&mut self` - Write access to the contract's state.
+    /// * `account` - Account to burn tokens from.
+    /// * `token_id` - Token id to be burnt.
+    /// * `value` - Amount to be burnt.
+    ///
+    /// # Errors
+    ///
+    /// If the caller is not `account` address and the `account` has not been
+    /// approved, then the error [`Error::MissingApprovalForAll`] is
+    /// returned.
+    /// 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.
+    fn burn(
+        &mut self,
+        account: Address,
+        token_id: U256,
+        value: U256,
+    ) -> Result<(), Self::Error>;
+
+    /// Destroys a batch of tokens from `account`.
+    ///
+    /// # Arguments
+    ///
+    /// * `&mut self` - Write access to the contract's state.
+    /// * `account` - Accounts to burn tokens from.
+    /// * `token_ids` - All token id to be burnt.
+    /// * `values` - All amount to be burnt.
+    ///
+    /// # Errors
+    ///
+    /// If the caller is not `account` address and the `account` has not been
+    /// approved, then the error [`Error::MissingApprovalForAll`] 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 any of the `values` is greater than the balance of the respective
+    /// token from `tokens` of the `from` account, then the error
+    /// [`Error::InsufficientBalance`] is returned.
+    fn burn_batch(
+        &mut self,
+        account: Address,
+        token_ids: Vec<U256>,
+        values: Vec<U256>,
+    ) -> Result<(), Self::Error>;
+}
+
+impl IErc1155Burnable for Erc1155 {
+    type Error = Error;
+
+    fn burn(
+        &mut self,
+        account: Address,
+        token_id: U256,
+        value: U256,
+    ) -> Result<(), Self::Error> {
+        self.ensure_approved_or_owner(account)?;
+        self._burn(account, token_id, value)
+    }
+
+    fn burn_batch(
+        &mut self,
+        account: Address,
+        token_ids: Vec<U256>,
+        values: Vec<U256>,
+    ) -> Result<(), Self::Error> {
+        self.ensure_approved_or_owner(account)?;
+        self._burn_batch(account, token_ids, values)
+    }
+}
+
+impl Erc1155 {
+    fn ensure_approved_or_owner(&self, account: Address) -> Result<(), Error> {
+        let sender = msg::sender();
+        if account != sender && !self.is_approved_for_all(account, sender) {
+            return Err(Error::MissingApprovalForAll(
+                ERC1155MissingApprovalForAll {
+                    owner: account,
+                    operator: sender,
+                },
+            ));
+        }
+        Ok(())
+    }
+}
+
+#[cfg(all(test, feature = "std"))]
+mod tests {
+    use alloy_primitives::{address, Address, U256};
+    use stylus_sdk::msg;
+
+    use super::IErc1155Burnable;
+    use crate::token::erc1155::{
+        tests::{random_token_ids, random_values},
+        ERC1155InsufficientBalance, ERC1155InvalidSender,
+        ERC1155MissingApprovalForAll, Erc1155, Error, IErc1155,
+    };
+
+    const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526");
+
+    fn init(
+        contract: &mut Erc1155,
+        receiver: Address,
+        size: usize,
+    ) -> (Vec<U256>, Vec<U256>) {
+        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)
+    }
+
+    #[motsu::test]
+    fn burns(contract: Erc1155) {
+        let alice = msg::sender();
+        let (token_ids, values) = init(contract, alice, 1);
+
+        let initial_balance = contract.balance_of(alice, token_ids[0]);
+        assert_eq!(values[0], initial_balance);
+
+        contract
+            .burn(alice, token_ids[0], values[0])
+            .expect("should burn own tokens");
+
+        let balance = contract.balance_of(alice, token_ids[0]);
+        assert_eq!(U256::ZERO, balance);
+    }
+
+    #[motsu::test]
+    fn burns_with_approval(contract: Erc1155) {
+        let alice = msg::sender();
+        let (token_ids, values) = init(contract, BOB, 1);
+
+        let initial_balance = contract.balance_of(BOB, token_ids[0]);
+        assert_eq!(values[0], initial_balance);
+
+        contract._operator_approvals.setter(BOB).setter(alice).set(true);
+
+        contract
+            .burn(BOB, token_ids[0], values[0])
+            .expect("should burn Bob's token");
+
+        let balance = contract.balance_of(BOB, token_ids[0]);
+        assert_eq!(U256::ZERO, balance);
+    }
+
+    #[motsu::test]
+    fn error_when_missing_approval_burns(contract: Erc1155) {
+        let alice = msg::sender();
+        let (token_ids, values) = init(contract, BOB, 1);
+
+        let err = contract
+            .burn(BOB, token_ids[0], values[0])
+            .expect_err("should not burn tokens without approval");
+
+        assert!(matches!(
+            err,
+            Error::MissingApprovalForAll(ERC1155MissingApprovalForAll {
+                owner,
+                operator
+            }) if owner == BOB && operator == alice
+        ));
+    }
+
+    #[motsu::test]
+    fn error_when_invalid_sender_burns(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
+            .burn(invalid_sender, token_ids[0], values[0])
+            .expect_err("should not burn tokens from the `Address::ZERO`");
+
+        assert!(matches!(
+            err,
+            Error::InvalidSender(ERC1155InvalidSender {
+                sender,
+            }) if sender == invalid_sender
+        ));
+    }
+
+    #[motsu::test]
+    fn error_when_insufficient_balance_burn(contract: Erc1155) {
+        let alice = msg::sender();
+        let (token_ids, values) = init(contract, alice, 1);
+
+        let token_id = token_ids[0];
+        let value = values[0];
+        let to_burn = value + U256::from(1);
+
+        let err = contract
+            .burn(alice, token_id, to_burn)
+            .expect_err("should return `ERC1155InsufficientBalance`");
+
+        assert!(matches!(
+            err,
+            Error::InsufficientBalance(ERC1155InsufficientBalance {
+                sender,
+                balance,
+                needed,
+                token_id,
+            }) if sender == alice && balance == value && needed == to_burn && token_id == token_id
+        ));
+    }
+
+    #[motsu::test]
+    fn burns_batch(contract: Erc1155) {
+        let alice = msg::sender();
+        let (token_ids, values) = init(contract, alice, 4);
+
+        for (&token_id, &value) in token_ids.iter().zip(values.iter()) {
+            let balance = contract.balance_of(alice, token_id);
+            assert_eq!(value, balance);
+        }
+
+        contract
+            .burn_batch(alice, token_ids.clone(), values.clone())
+            .expect("should burn own tokens in batch");
+
+        for token_id in token_ids {
+            let balance = contract.balance_of(alice, token_id);
+            assert_eq!(U256::ZERO, balance);
+        }
+    }
+
+    #[motsu::test]
+    fn burns_batch_with_approval(contract: Erc1155) {
+        let alice = msg::sender();
+        let (token_ids, values) = init(contract, BOB, 4);
+
+        for (&token_id, &value) in token_ids.iter().zip(values.iter()) {
+            let balance = contract.balance_of(BOB, token_id);
+            assert_eq!(value, balance);
+        }
+
+        contract._operator_approvals.setter(BOB).setter(alice).set(true);
+
+        contract
+            .burn_batch(BOB, token_ids.clone(), values.clone())
+            .expect("should burn Bob's tokens in batch");
+
+        for token_id in token_ids {
+            let balance = contract.balance_of(BOB, token_id);
+            assert_eq!(U256::ZERO, balance);
+        }
+    }
+
+    #[motsu::test]
+    fn error_when_missing_approval_burn_batch(contract: Erc1155) {
+        let alice = msg::sender();
+        let (token_ids, values) = init(contract, BOB, 2);
+
+        let err = contract
+            .burn_batch(BOB, token_ids.clone(), values.clone())
+            .expect_err("should not burn tokens in batch without approval");
+
+        assert!(matches!(
+            err,
+            Error::MissingApprovalForAll(ERC1155MissingApprovalForAll {
+                owner,
+                operator
+            }) if owner == BOB && operator == alice
+        ));
+    }
+
+    #[motsu::test]
+    fn error_when_invalid_sender_burn_batch(contract: Erc1155) {
+        let alice = msg::sender();
+        let (token_ids, values) = init(contract, alice, 5);
+        let invalid_sender = Address::ZERO;
+
+        contract
+            ._operator_approvals
+            .setter(invalid_sender)
+            .setter(alice)
+            .set(true);
+
+        let err = contract
+            .burn_batch(invalid_sender, token_ids.clone(), values.clone())
+            .expect_err(
+                "should not burn tokens in batch from the `Address::ZERO`",
+            );
+
+        assert!(matches!(
+            err,
+            Error::InvalidSender(ERC1155InvalidSender {
+                sender,
+            }) if sender == invalid_sender
+        ));
+    }
+
+    #[motsu::test]
+    fn error_when_insufficient_balance_burn_batch(contract: Erc1155) {
+        let alice = msg::sender();
+        let (token_ids, values) = init(contract, alice, 5);
+        let to_burn: Vec<U256> =
+            values.iter().map(|v| v + U256::from(1)).collect();
+
+        let err = contract
+            .burn_batch(alice, token_ids.clone(), to_burn.clone())
+            .expect_err("should return `ERC1155InsufficientBalance`");
+
+        assert!(matches!(
+            err,
+            Error::InsufficientBalance(ERC1155InsufficientBalance {
+                sender,
+                balance,
+                needed,
+                token_id,
+            }) if sender == alice && balance == values[0] && needed == to_burn[0] && token_id == token_ids[0]
+        ));
+    }
+}
diff --git a/contracts/src/token/erc1155/extensions/mod.rs b/contracts/src/token/erc1155/extensions/mod.rs
new file mode 100644
index 0000000..4ed8626
--- /dev/null
+++ b/contracts/src/token/erc1155/extensions/mod.rs
@@ -0,0 +1,4 @@
+//! Common extensions to the ERC-1155 standard.
+pub mod burnable;
+
+pub use burnable::IErc1155Burnable;
diff --git a/contracts/src/token/erc1155/mod.rs b/contracts/src/token/erc1155/mod.rs
index e128f68..1bd9277 100644
--- a/contracts/src/token/erc1155/mod.rs
+++ b/contracts/src/token/erc1155/mod.rs
@@ -1,13 +1,13 @@
 //! Implementation of the ERC-1155 token standard.
 use alloc::{vec, vec::Vec};
 
-use alloy_primitives::{fixed_bytes, Address, FixedBytes, U256};
+use alloy_primitives::{Address, FixedBytes, U256};
 use openzeppelin_stylus_proc::interface_id;
 use stylus_sdk::{
     abi::Bytes,
     alloy_sol_types::sol,
     call::{self, Call, MethodError},
-    evm, msg,
+    evm, function_selector, msg,
     prelude::{public, sol_storage, AddressVM, SolidityError},
     storage::TopLevelStorage,
 };
@@ -17,20 +17,28 @@ use crate::utils::{
     math::storage::SubAssignUnchecked,
 };
 
+pub mod extensions;
+
 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");
+const SINGLE_TRANSFER_FN_SELECTOR: [u8; 4] = function_selector!(
+    "onERC1155Received",
+    Address,
+    Address,
+    U256,
+    U256,
+    Bytes
+);
+
+const BATCH_TRANSFER_FN_SELECTOR: [u8; 4] = function_selector!(
+    "onERC1155BatchReceived",
+    Address,
+    Address,
+    Vec<U256>,
+    Vec<U256>,
+    Bytes
+);
 
 sol! {
     /// Emitted when `value` amount of tokens of type `id` are
@@ -687,8 +695,9 @@ impl Erc1155 {
     /// [`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 any of the `values` is greater than the balance of the respective
+    /// token from `tokens` of the `from` account, then the error
+    /// [`Error::InsufficientBalance`] is returned.
     ///
     /// # Events
     ///
@@ -895,8 +904,9 @@ impl Erc1155 {
     /// [`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 any of the `values` is greater than the balance of the respective
+    /// token from `tokens` of the `from` account, then the error
+    /// [`Error::InsufficientBalance`] is returned.
     ///
     /// # Events
     ///
@@ -1092,7 +1102,7 @@ impl Erc1155 {
 /// implements [`IERC1155Receiver`] interface.
 struct Erc1155ReceiverData {
     /// ERC-1155 Receiver function selector.
-    receiver_fn_selector: FixedBytes<4>,
+    receiver_fn_selector: [u8; 4],
     /// Transfer details, either [`Transfer::Single`] or [`Transfer::Batch`].
     transfer: Transfer,
 }
@@ -1193,7 +1203,7 @@ mod tests {
     }
 
     pub(crate) fn random_values(size: usize) -> Vec<U256> {
-        (0..size).map(|_| U256::from(rand::random::<u128>())).collect()
+        (1..size + 1).map(U256::from).collect()
     }
 
     fn init(
diff --git a/contracts/src/token/erc20/extensions/burnable.rs b/contracts/src/token/erc20/extensions/burnable.rs
index fce7fe9..6835317 100644
--- a/contracts/src/token/erc20/extensions/burnable.rs
+++ b/contracts/src/token/erc20/extensions/burnable.rs
@@ -12,7 +12,7 @@ pub trait IErc20Burnable {
     /// The error type associated to this ERC-20 Burnable trait implementation.
     type Error: Into<alloc::vec::Vec<u8>>;
 
-    /// Destroys a `value` amount of tokens from the caller. lowering the total
+    /// Destroys a `value` amount of tokens from the caller, lowering the total
     /// supply.
     ///
     /// Relies on the `update` mechanism.
diff --git a/docs/modules/ROOT/pages/ERC1155.adoc b/docs/modules/ROOT/pages/ERC1155.adoc
index d217219..9c16d03 100644
--- a/docs/modules/ROOT/pages/ERC1155.adoc
+++ b/docs/modules/ROOT/pages/ERC1155.adoc
@@ -1,3 +1,10 @@
 = 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].
+
+[[erc1155-token-extensions]]
+== Extensions
+
+Additionally, there are multiple custom extensions, including:
+
+* xref:erc1155-burnable.adoc[ERC-1155 Burnable]: Optional Burnable extension of the ERC-1155 standard.
\ No newline at end of file
diff --git a/docs/modules/ROOT/pages/erc1155-burnable.adoc b/docs/modules/ROOT/pages/erc1155-burnable.adoc
new file mode 100644
index 0000000..546b2e8
--- /dev/null
+++ b/docs/modules/ROOT/pages/erc1155-burnable.adoc
@@ -0,0 +1,50 @@
+= ERC-1155 Burnable
+
+Extension of xref:erc1155.adoc[ERC-1155] that allows token holders to destroy both their
+own tokens and those that they have been approved to use.
+
+[[usage]]
+== Usage
+
+In order to make https://docs.rs/openzeppelin-stylus/0.2.0-alpha.2/openzeppelin_stylus/token/erc1155/extensions/burnable/index.html[`ERC-1155 Burnable`] methods “external” so that other contracts can call them, you need to implement them by yourself for your final contract as follows:
+
+[source,rust]
+----
+use openzeppelin_stylus::token::erc1155::{
+    extensions::IErc1155Burnable, Erc1155,
+};
+
+sol_storage! {
+    #[entrypoint]
+    struct Erc1155Example {
+        #[borrow]
+        Erc1155 erc1155;
+    }
+}
+
+#[public]
+#[inherit(Erc1155)]
+impl Erc1155Example {
+    fn burn(
+        &mut self,
+        account: Address,
+        token_id: U256,
+        value: U256,
+    ) -> Result<(), Vec<u8>> {
+        // ...
+        self.erc1155.burn(account, token_id, value)?;
+        // ...
+    }
+
+    fn burn_batch(
+        &mut self,
+        account: Address,
+        token_ids: Vec<U256>,
+        values: Vec<U256>,
+    ) -> Result<(), Vec<u8>> {
+        // ...
+        self.erc1155.burn_batch(account, token_ids, values)?;
+        // ...
+    }
+}
+----
diff --git a/examples/erc1155/Cargo.toml b/examples/erc1155/Cargo.toml
index 1bb24e3..9fd21fd 100644
--- a/examples/erc1155/Cargo.toml
+++ b/examples/erc1155/Cargo.toml
@@ -16,7 +16,6 @@ alloy.workspace = true
 e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
-rand.workspace = true
 
 [features]
 e2e = []
diff --git a/examples/erc1155/src/lib.rs b/examples/erc1155/src/lib.rs
index d3fa59c..116b141 100644
--- a/examples/erc1155/src/lib.rs
+++ b/examples/erc1155/src/lib.rs
@@ -4,7 +4,9 @@ extern crate alloc;
 use alloc::vec::Vec;
 
 use alloy_primitives::{Address, U256};
-use openzeppelin_stylus::token::erc1155::Erc1155;
+use openzeppelin_stylus::token::erc1155::{
+    extensions::IErc1155Burnable, Erc1155,
+};
 use stylus_sdk::{
     abi::Bytes,
     prelude::{entrypoint, public, sol_storage},
@@ -42,4 +44,38 @@ impl Erc1155Example {
         self.erc1155._mint_batch(to, token_ids, amounts, &data)?;
         Ok(())
     }
+
+    pub fn set_operator_approvals(
+        &mut self,
+        owner: Address,
+        operator: Address,
+        approved: bool,
+    ) -> Result<(), Vec<u8>> {
+        self.erc1155
+            ._operator_approvals
+            .setter(owner)
+            .setter(operator)
+            .set(approved);
+        Ok(())
+    }
+
+    fn burn(
+        &mut self,
+        account: Address,
+        token_id: U256,
+        value: U256,
+    ) -> Result<(), Vec<u8>> {
+        self.erc1155.burn(account, token_id, value)?;
+        Ok(())
+    }
+
+    fn burn_batch(
+        &mut self,
+        account: Address,
+        token_ids: Vec<U256>,
+        values: Vec<U256>,
+    ) -> Result<(), Vec<u8>> {
+        self.erc1155.burn_batch(account, token_ids, values)?;
+        Ok(())
+    }
 }


Test patch:
diff --git a/examples/erc1155/tests/abi/mod.rs b/examples/erc1155/tests/abi/mod.rs
index b7b1321..6e83763 100644
--- a/examples/erc1155/tests/abi/mod.rs
+++ b/examples/erc1155/tests/abi/mod.rs
@@ -13,13 +13,15 @@ sol!(
         function safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data) external;
         function mint(address to, uint256 id, uint256 amount, bytes memory data) external;
         function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) external;
+        function burn(address account, uint256 id, uint256 value) external;
+        function burnBatch(address account, uint256[] memory ids, uint256[] memory values) external;
 
         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);
+        error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId);
 
         #[derive(Debug, PartialEq)]
         event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
diff --git a/examples/erc1155/tests/erc1155.rs b/examples/erc1155/tests/erc1155.rs
index d693e56..6479a1d 100644
--- a/examples/erc1155/tests/erc1155.rs
+++ b/examples/erc1155/tests/erc1155.rs
@@ -13,7 +13,7 @@ fn random_token_ids(size: usize) -> Vec<U256> {
 }
 
 fn random_values(size: usize) -> Vec<U256> {
-    (0..size).map(|_| U256::from(rand::random::<u128>())).collect()
+    (1..size + 1).map(U256::from).collect()
 }
 
 // ============================================================================
@@ -61,7 +61,7 @@ async fn balance_of_zero_balance(alice: Account) -> eyre::Result<()> {
 
     let Erc1155::balanceOfReturn { balance } =
         contract.balanceOf(alice.address(), token_ids[0]).call().await?;
-    assert_eq!(uint!(0_U256), balance);
+    assert_eq!(U256::ZERO, balance);
 
     Ok(())
 }
@@ -81,10 +81,7 @@ async fn balance_of_batch_zero_balance(
 
     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
-    );
+    assert_eq!(vec![U256::ZERO, U256::ZERO, U256::ZERO, U256::ZERO], balances);
 
     Ok(())
 }
@@ -1029,7 +1026,7 @@ async fn error_when_insufficient_balance_safe_transfer_from(
         sender: bob_addr,
         balance: value,
         needed: value + uint!(1_U256),
-        id: token_id
+        tokenId: token_id
     }));
 
     Ok(())
@@ -1563,7 +1560,299 @@ async fn error_when_insufficient_balance_safe_batch_transfer_from(
         sender: bob_addr,
         balance: values[0],
         needed: values[0] + uint!(1_U256),
-        id: token_ids[0]
+        tokenId: token_ids[0]
+    }));
+
+    Ok(())
+}
+
+// ============================================================================
+// Integration Tests: ERC-1155 Burnable Extension
+// ============================================================================
+
+#[e2e::test]
+async fn burns(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(1);
+    let values = random_values(1);
+
+    let _ = watch!(contract.mint(
+        alice_addr,
+        token_ids[0],
+        values[0],
+        vec![].into()
+    ));
+
+    let initial_balance =
+        contract.balanceOf(alice_addr, token_ids[0]).call().await?.balance;
+    assert_eq!(values[0], initial_balance);
+
+    let receipt = receipt!(contract.burn(alice_addr, token_ids[0], values[0]))?;
+
+    assert!(receipt.emits(Erc1155::TransferSingle {
+        operator: alice_addr,
+        from: alice_addr,
+        to: Address::ZERO,
+        id: token_ids[0],
+        value: values[0],
+    }));
+
+    let balance =
+        contract.balanceOf(alice_addr, token_ids[0]).call().await?.balance;
+    assert_eq!(U256::ZERO, balance);
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn burns_with_approval(alice: Account, bob: Account) -> eyre::Result<()> {
+    let contract_addr = alice.as_deployer().deploy().await?.address()?;
+    let contract = 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_ids = random_token_ids(1);
+    let values = random_values(1);
+
+    let _ =
+        watch!(contract.mint(bob_addr, token_ids[0], values[0], vec![].into()));
+
+    let initial_balance =
+        contract.balanceOf(bob_addr, token_ids[0]).call().await?.balance;
+    assert_eq!(values[0], initial_balance);
+
+    let _ = watch!(contract_bob.setApprovalForAll(alice_addr, true));
+
+    let receipt = receipt!(contract.burn(bob_addr, token_ids[0], values[0]))?;
+
+    assert!(receipt.emits(Erc1155::TransferSingle {
+        operator: alice_addr,
+        from: bob_addr,
+        to: Address::ZERO,
+        id: token_ids[0],
+        value: values[0],
+    }));
+
+    let balance =
+        contract.balanceOf(bob_addr, token_ids[0]).call().await?.balance;
+    assert_eq!(U256::ZERO, balance);
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn error_when_missing_approval_burn(
+    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_ids = random_token_ids(1);
+    let values = random_values(1);
+
+    let _ =
+        watch!(contract.mint(bob_addr, token_ids[0], values[0], vec![].into()));
+
+    let err = send!(contract.burn(bob_addr, token_ids[0], values[0]))
+        .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_burn(
+    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 to_burn = value + uint!(1_U256);
+
+    let _ = watch!(contract.mint(alice_addr, token_id, value, vec![].into()));
+
+    let err = send!(contract.burn(alice_addr, token_id, to_burn))
+        .expect_err("should return `ERC1155InsufficientBalance`");
+
+    assert!(err.reverted_with(Erc1155::ERC1155InsufficientBalance {
+        sender: alice_addr,
+        balance: value,
+        needed: to_burn,
+        tokenId: token_id
+    }));
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn burns_batch(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(4);
+    let values = random_values(4);
+
+    let _ = watch!(contract.mintBatch(
+        alice_addr,
+        token_ids.clone(),
+        values.clone(),
+        vec![].into()
+    ));
+
+    for (&id, &value) in token_ids.iter().zip(values.iter()) {
+        let balance = contract.balanceOf(alice_addr, id).call().await?.balance;
+        assert_eq!(value, balance);
+    }
+
+    let receipt = receipt!(contract.burnBatch(
+        alice_addr,
+        token_ids.clone(),
+        values.clone()
+    ))?;
+
+    assert!(receipt.emits(Erc1155::TransferBatch {
+        operator: alice_addr,
+        from: alice_addr,
+        to: Address::ZERO,
+        ids: token_ids.clone(),
+        values,
+    }));
+
+    for id in token_ids {
+        let balance = contract.balanceOf(alice_addr, id).call().await?.balance;
+        assert_eq!(U256::ZERO, balance);
+    }
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn burns_batch_with_approval(
+    alice: Account,
+    bob: Account,
+) -> eyre::Result<()> {
+    let contract_addr = alice.as_deployer().deploy().await?.address()?;
+    let contract = 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_ids = random_token_ids(4);
+    let values = random_values(4);
+
+    let _ = watch!(contract.mintBatch(
+        bob_addr,
+        token_ids.clone(),
+        values.clone(),
+        vec![].into()
+    ));
+
+    for (&id, &value) in token_ids.iter().zip(values.iter()) {
+        let balance = contract.balanceOf(bob_addr, id).call().await?.balance;
+        assert_eq!(value, balance);
+    }
+
+    let _ = watch!(contract_bob.setApprovalForAll(alice_addr, true));
+
+    let receipt = receipt!(contract.burnBatch(
+        bob_addr,
+        token_ids.clone(),
+        values.clone()
+    ))?;
+
+    assert!(receipt.emits(Erc1155::TransferBatch {
+        operator: alice_addr,
+        from: bob_addr,
+        to: Address::ZERO,
+        ids: token_ids.clone(),
+        values,
+    }));
+
+    for id in token_ids {
+        let balance = contract.balanceOf(bob_addr, id).call().await?.balance;
+        assert_eq!(U256::ZERO, balance);
+    }
+
+    Ok(())
+}
+
+#[e2e::test]
+async fn error_when_missing_approval_burn_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 alice_addr = alice.address();
+    let bob_addr = bob.address();
+    let token_ids = random_token_ids(2);
+    let values = random_values(2);
+
+    let _ = watch!(contract.mintBatch(
+        bob_addr,
+        token_ids.clone(),
+        values.clone(),
+        vec![].into()
+    ));
+
+    let err = send!(contract.burnBatch(bob_addr, token_ids, values))
+        .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_burn_batch(
+    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 to_burn: Vec<U256> = values.iter().map(|v| v + uint!(1_U256)).collect();
+
+    let _ = watch!(contract.mintBatch(
+        alice_addr,
+        token_ids.clone(),
+        values.clone(),
+        vec![].into()
+    ));
+
+    let err = send!(contract.burnBatch(
+        alice_addr,
+        token_ids.clone(),
+        to_burn.clone()
+    ))
+    .expect_err("should return `ERC1155InsufficientBalance`");
+
+    assert!(err.reverted_with(Erc1155::ERC1155InsufficientBalance {
+        sender: alice_addr,
+        balance: values[0],
+        needed: to_burn[0],
+        tokenId: token_ids[0]
     }));
 
     Ok(())