diff --git a/Cargo.lock b/Cargo.lock index a06ba21c3..737f2011d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,7 +37,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.64", "syn-solidity", "tiny-keccak", ] @@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" @@ -94,9 +94,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "const-hex" -version = "1.11.3" +version = "1.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba00838774b4ab0233e355d26710fbfc8327a05c017f6dc4873f876d1f79f78" +checksum = "70ff96486ccc291d36a958107caf2c0af8c78c0af7d31ae2f35ce055130de1a6" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -210,6 +210,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "erc20-burnable-example" +version = "0.0.0" +dependencies = [ + "alloy-primitives", + "contracts", + "mini-alloc", + "stylus-proc", + "stylus-sdk", +] + [[package]] name = "erc20-example" version = "0.0.0" @@ -244,9 +255,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if 1.0.0", "libc", @@ -269,7 +280,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.64", ] [[package]] @@ -365,9 +376,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -398,9 +409,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -421,9 +432,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -527,28 +538,28 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.64", ] [[package]] @@ -609,9 +620,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f" dependencies = [ "proc-macro2", "quote", @@ -626,7 +637,7 @@ checksum = "e5f995d2140b0f751dbe94365be2591edbf3d1b75dcfaeac14183abbd2ff07bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.64", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cd6c987cf..5db03d312 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,14 @@ [workspace] members = [ - "contracts", - "lib/crypto", - "lib/grip", - "lib/grip-proc", - "examples/erc20", - "examples/erc721", - "examples/merkle-proofs", - "examples/ownable", + "contracts", + "lib/crypto", + "lib/grip", + "lib/grip-proc", + "examples/erc20", + "examples/erc721", + "examples/merkle-proofs", + "examples/ownable", + "examples/erc20-burnable", ] # Explicitly set the resolver to version 2, which is the default for packages # with edition >= 2021. diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 3cb58cdfa..9888fbfad 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -28,7 +28,6 @@ default = [] # ERC-20 erc20 = [] erc20_metadata = ["erc20"] -erc20_burnable = ["erc20"] # ERC-721 erc721 = [] diff --git a/contracts/src/erc20/extensions/burnable.rs b/contracts/src/erc20/extensions/burnable.rs deleted file mode 100644 index ee122b8c9..000000000 --- a/contracts/src/erc20/extensions/burnable.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! Optional Burnable extension of the ERC-20 standard. - -/// This macro provides an implementation of the ERC-20 Burnable extension. -/// -/// It adds the `burn` and `burn_from` functions, and expects the token -/// to contain `ERC20 erc20` as a field. See [`crate::ERC20`]. -#[macro_export] -macro_rules! erc20_burnable_impl { - () => { - /// Destroys a `value` amount of tokens from the caller. - /// lowering the total supply. - /// - /// Relies on the `update` mechanism. - /// - /// # Arguments - /// - /// * `value` - Amount to be burnt. - /// - /// # Errors - /// - /// If the `from` address doesn't have enough tokens, then the error - /// [`Error::InsufficientBalance`] is returned. - /// - /// # Events - /// - /// Emits a [`Transfer`] event. - pub(crate) fn burn( - &mut self, - value: alloy_primitives::U256, - ) -> Result<(), alloc::vec::Vec> { - self.erc20 - ._burn(stylus_sdk::msg::sender(), value) - .map_err(|e| e.into()) - } - - /// Destroys a `value` amount of tokens from `account`, - /// lowering the total supply. - /// - /// Relies on the `update` mechanism. - /// - /// # Arguments - /// - /// * `account` - Owner's address. - /// * `value` - Amount to be burnt. - /// - /// # Errors - /// - /// If not enough allowance is available, then the error - /// [`Error::InsufficientAllowance`] is returned. - /// * If the `from` address is `Address::ZERO`, then the error - /// [`Error::InvalidSender`] is returned. - /// If the `from` address doesn't have enough tokens, then the error - /// [`Error::InsufficientBalance`] is returned. - /// - /// # Events - /// - /// Emits a [`Transfer`] event. - pub(crate) fn burn_from( - &mut self, - account: alloy_primitives::Address, - value: alloy_primitives::U256, - ) -> Result<(), alloc::vec::Vec> { - self.erc20._spend_allowance( - account, - stylus_sdk::msg::sender(), - value, - )?; - self.erc20._burn(account, value).map_err(|e| e.into()) - } - }; -} - -#[cfg(test)] -mod tests { - use alloy_primitives::{address, Address, U256}; - use stylus_sdk::{msg, prelude::*}; - - use crate::erc20::{ - ERC20InsufficientAllowance, ERC20InsufficientBalance, - ERC20InvalidSender, Error, ERC20, - }; - - sol_storage! { - pub struct TestERC20Burnable { - ERC20 erc20; - } - } - - #[external] - #[inherit(ERC20)] - impl TestERC20Burnable { - erc20_burnable_impl!(); - } - - impl Default for TestERC20Burnable { - fn default() -> Self { - Self { erc20: ERC20::default() } - } - } - - #[grip::test] - fn burns(contract: TestERC20Burnable) { - let zero = U256::ZERO; - let one = U256::from(1); - - assert_eq!(zero, contract.erc20.total_supply()); - - // Mint some tokens for msg::sender(). - let sender = msg::sender(); - - let two = U256::from(2); - contract.erc20._update(Address::ZERO, sender, two).unwrap(); - assert_eq!(two, contract.erc20.balance_of(sender)); - assert_eq!(two, contract.erc20.total_supply()); - - contract.burn(one).unwrap(); - - assert_eq!(one, contract.erc20.balance_of(sender)); - assert_eq!(one, contract.erc20.total_supply()); - } - - #[grip::test] - fn burns_errors_when_insufficient_balance(contract: TestERC20Burnable) { - let zero = U256::ZERO; - let one = U256::from(1); - let sender = msg::sender(); - - assert_eq!(zero, contract.erc20.balance_of(sender)); - - let result = contract.burn(one); - let expected_err: alloc::vec::Vec = - Error::InsufficientBalance(ERC20InsufficientBalance { - sender, - balance: zero, - needed: one, - }) - .into(); - - assert_eq!(result.unwrap_err(), expected_err); - } - #[grip::test] - fn burn_from(contract: TestERC20Burnable) { - let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); - let sender = msg::sender(); - - // Alice approves `msg::sender`. - let one = U256::from(1); - contract.erc20._allowances.setter(alice).setter(sender).set(one); - - // Mint some tokens for Alice. - let two = U256::from(2); - contract.erc20._update(Address::ZERO, alice, two).unwrap(); - assert_eq!(two, contract.erc20.balance_of(alice)); - assert_eq!(two, contract.erc20.total_supply()); - - contract.burn_from(alice, one).unwrap(); - - assert_eq!(one, contract.erc20.balance_of(alice)); - assert_eq!(one, contract.erc20.total_supply()); - assert_eq!(U256::ZERO, contract.erc20.allowance(alice, sender)); - } - - #[grip::test] - fn burns_from_errors_when_insufficient_balance( - contract: TestERC20Burnable, - ) { - let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); - - // Alice approves `msg::sender`. - let zero = U256::ZERO; - let one = U256::from(1); - contract.erc20._allowances.setter(alice).setter(msg::sender()).set(one); - assert_eq!(zero, contract.erc20.balance_of(alice)); - - let one = U256::from(1); - let result = contract.burn_from(alice, one); - let expected_err: alloc::vec::Vec = - Error::InsufficientBalance(ERC20InsufficientBalance { - sender: alice, - balance: zero, - needed: one, - }) - .into(); - - assert_eq!(result.unwrap_err(), expected_err); - } - - #[grip::test] - fn burns_from_errors_when_invalid_sender(contract: TestERC20Burnable) { - let one = U256::from(1); - contract - .erc20 - ._allowances - .setter(Address::ZERO) - .setter(msg::sender()) - .set(one); - let result = contract.burn_from(Address::ZERO, one); - let expected_err: alloc::vec::Vec = - Error::InvalidSender(ERC20InvalidSender { sender: Address::ZERO }) - .into(); - - assert_eq!(result.unwrap_err(), expected_err); - } - - #[grip::test] - fn burns_from_errors_when_insufficient_allowance( - contract: TestERC20Burnable, - ) { - let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d"); - - // Mint some tokens for Alice. - let one = U256::from(1); - contract.erc20._update(Address::ZERO, alice, one).unwrap(); - assert_eq!(one, contract.erc20.balance_of(alice)); - - let result = contract.burn_from(alice, one); - let expected_err: alloc::vec::Vec = - Error::InsufficientAllowance(ERC20InsufficientAllowance { - spender: msg::sender(), - allowance: U256::ZERO, - needed: one, - }) - .into(); - - assert_eq!(result.unwrap_err(), expected_err); - } -} diff --git a/contracts/src/erc20/extensions/mod.rs b/contracts/src/erc20/extensions/mod.rs index 75a327110..fcf1c1f59 100644 --- a/contracts/src/erc20/extensions/mod.rs +++ b/contracts/src/erc20/extensions/mod.rs @@ -6,8 +6,3 @@ cfg_if::cfg_if! { pub use metadata::ERC20Metadata; } } -cfg_if::cfg_if! { - if #[cfg(any(test, feature = "erc20_burnable"))] { - pub mod burnable; - } -} diff --git a/examples/erc20-burnable/Cargo.toml b/examples/erc20-burnable/Cargo.toml new file mode 100644 index 000000000..0e876c36e --- /dev/null +++ b/examples/erc20-burnable/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "erc20-burnable-example" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version = "0.0.0" + +[dependencies] +contracts = { path = "../../contracts", features = ["erc20"] } +alloy-primitives.workspace = true +stylus-sdk.workspace = true +stylus-proc.workspace = true +mini-alloc.workspace = true + +[lib] +crate-type = ["lib", "cdylib"] diff --git a/examples/erc20-burnable/src/lib.rs b/examples/erc20-burnable/src/lib.rs new file mode 100644 index 000000000..f1d78cfbb --- /dev/null +++ b/examples/erc20-burnable/src/lib.rs @@ -0,0 +1,81 @@ +#![cfg_attr(not(test), no_main, no_std)] +extern crate alloc; + +// That is example implementation of ERC20 with Burnable flavour. +// Extension of ERC20 that allows token holders to destroy both their own +// tokens and those that they have an allowance for, in a way that can be +// recognized off-chain (via event analysis). + +use contracts::erc20::ERC20; +use stylus_sdk::prelude::{entrypoint, external, sol_storage}; + +sol_storage! { + #[entrypoint] + struct Token { + #[borrow] + ERC20 erc20; + } +} + +#[external] +#[inherit(ERC20)] +impl Token { + /// Destroys a `value` amount of tokens from the caller. + /// lowering the total supply. + /// + /// Relies on the `update` mechanism. + /// + /// # Arguments + /// + /// * `value` - Amount to be burnt. + /// + /// # Errors + /// + /// If the `from` address doesn't have enough tokens, then the error + /// [`Error::InsufficientBalance`] is returned. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. + pub(crate) fn burn( + &mut self, + value: alloy_primitives::U256, + ) -> Result<(), alloc::vec::Vec> { + self.erc20._burn(stylus_sdk::msg::sender(), value).map_err(|e| e.into()) + } + + /// Destroys a `value` amount of tokens from `account`, + /// lowering the total supply. + /// + /// Relies on the `update` mechanism. + /// + /// # Arguments + /// + /// * `account` - Owner's address. + /// * `value` - Amount to be burnt. + /// + /// # Errors + /// + /// If not enough allowance is available, then the error + /// [`Error::InsufficientAllowance`] is returned. + /// * If the `from` address is `Address::ZERO`, then the error + /// [`Error::InvalidSender`] is returned. + /// If the `from` address doesn't have enough tokens, then the error + /// [`Error::InsufficientBalance`] is returned. + /// + /// # Events + /// + /// Emits a [`Transfer`] event. + pub(crate) fn burn_from( + &mut self, + account: alloy_primitives::Address, + value: alloy_primitives::U256, + ) -> Result<(), alloc::vec::Vec> { + self.erc20._spend_allowance( + account, + stylus_sdk::msg::sender(), + value, + )?; + self.erc20._burn(account, value).map_err(|e| e.into()) + } +} diff --git a/examples/erc20/Cargo.toml b/examples/erc20/Cargo.toml index 2f1e7da20..7b6dbab99 100644 --- a/examples/erc20/Cargo.toml +++ b/examples/erc20/Cargo.toml @@ -7,7 +7,7 @@ publish = false version = "0.0.0" [dependencies] -contracts = { path = "../../contracts", features = ["erc20_metadata", "erc20_burnable"] } +contracts = { path = "../../contracts", features = ["erc20_metadata"] } alloy-primitives.workspace = true stylus-sdk.workspace = true stylus-proc.workspace = true diff --git a/examples/erc20/src/lib.rs b/examples/erc20/src/lib.rs index d16f87103..77aad9237 100644 --- a/examples/erc20/src/lib.rs +++ b/examples/erc20/src/lib.rs @@ -5,7 +5,6 @@ use alloc::string::String; use contracts::{ erc20::{extensions::ERC20Metadata, ERC20}, - erc20_burnable_impl, }; use stylus_sdk::prelude::{entrypoint, external, sol_storage}; @@ -24,10 +23,6 @@ sol_storage! { #[external] #[inherit(ERC20, ERC20Metadata)] impl Token { - // This macro implements ERC20Burnable functions -- `burn` and `burn_from`. - // Expects an `ERC20 erc20` as a field of `Token`. - erc20_burnable_impl!(); - pub fn constructor(&mut self, name: String, symbol: String) { self.metadata.constructor(name, symbol); }