-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(erc20): implement ERC20Burnable extension #31
Changes from 16 commits
71df2a2
a4b80d4
dc04ea3
af75199
49c8d58
19ffde4
3557267
4a1640e
26b0243
1da8c70
a586a3a
65f7a75
53c4aef
c4f2ed0
43e4341
907e6da
a7d9835
ad6488f
101af71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,192 @@ | ||||||||||||||||||||||||||||||||||
//! Optional Burnable extension of the ERC-20 standard. | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
#[macro_export] | ||||||||||||||||||||||||||||||||||
/// This macro provides implementation of ERC-20 Burnable extension. | ||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||
/// It adds `burn` and `burn_from` function | ||||||||||||||||||||||||||||||||||
/// to a custom token that contains `ERC20 erc20` attribute. | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||
/// Requires import of: | ||||||||||||||||||||||||||||||||||
/// * alloy_primitives::{Address, U256} | ||||||||||||||||||||||||||||||||||
Check warning on line 10 in contracts/src/erc20/extensions/burnable.rs GitHub Actions / clippy[clippy] contracts/src/erc20/extensions/burnable.rs#L10
Raw output
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use fully qualified paths inside the macro instead of keeping this as a comment. I'll make the suggestions in the following comments. |
||||||||||||||||||||||||||||||||||
/// * contracts::erc20::{Error, ERC20} | ||||||||||||||||||||||||||||||||||
Check warning on line 11 in contracts/src/erc20/extensions/burnable.rs GitHub Actions / clippy[clippy] contracts/src/erc20/extensions/burnable.rs#L11
Raw output
|
||||||||||||||||||||||||||||||||||
/// * stylus_sdk::msg | ||||||||||||||||||||||||||||||||||
Check warning on line 12 in contracts/src/erc20/extensions/burnable.rs GitHub Actions / clippy[clippy] contracts/src/erc20/extensions/burnable.rs#L12
Raw output
|
||||||||||||||||||||||||||||||||||
macro_rules! impl_erc20_burnable { | ||||||||||||||||||||||||||||||||||
Check warning on line 13 in contracts/src/erc20/extensions/burnable.rs GitHub Actions / clippy[clippy] contracts/src/erc20/extensions/burnable.rs#L13
Raw output
|
||||||||||||||||||||||||||||||||||
() => { | ||||||||||||||||||||||||||||||||||
/// 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: U256) -> Result<(), Error> { | ||||||||||||||||||||||||||||||||||
self.erc20._burn(msg::sender(), value) | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/// 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: Address, | ||||||||||||||||||||||||||||||||||
value: U256, | ||||||||||||||||||||||||||||||||||
) -> Result<(), Error> { | ||||||||||||||||||||||||||||||||||
self.erc20._spend_allowance(account, msg::sender(), value)?; | ||||||||||||||||||||||||||||||||||
self.erc20._burn(account, value) | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a problem with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I feared that...not sure what the solution is 😢 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, fixed it! pub(crate) fn burn_from(
&mut self,
account: alloy_primitives::Address,
value: alloy_primitives::U256,
) -> Result<(), alloc::vec::Vec<u8>> {
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::{Error, ERC20}; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
sol_storage! { | ||||||||||||||||||||||||||||||||||
pub struct TestERC20Burnable { | ||||||||||||||||||||||||||||||||||
ERC20 erc20; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
#[external] | ||||||||||||||||||||||||||||||||||
#[inherit(ERC20)] | ||||||||||||||||||||||||||||||||||
impl TestERC20Burnable { | ||||||||||||||||||||||||||||||||||
impl_erc20_burnable!(); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
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 one = U256::from(1); | ||||||||||||||||||||||||||||||||||
let sender = msg::sender(); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
assert_eq!(U256::ZERO, contract.erc20.balance_of(sender)); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
let result = contract.burn(one); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
assert!(matches!(result, Err(Error::InsufficientBalance(_)))); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
#[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 one = U256::from(1); | ||||||||||||||||||||||||||||||||||
contract.erc20._allowances.setter(alice).setter(msg::sender()).set(one); | ||||||||||||||||||||||||||||||||||
assert_eq!(U256::ZERO, contract.erc20.balance_of(alice)); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
let one = U256::from(1); | ||||||||||||||||||||||||||||||||||
let result = contract.burn_from(alice, one); | ||||||||||||||||||||||||||||||||||
assert!(matches!(result, Err(Error::InsufficientBalance(_)))); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
#[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); | ||||||||||||||||||||||||||||||||||
assert!(matches!(result, Err(Error::InvalidSender(_)))); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
#[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); | ||||||||||||||||||||||||||||||||||
assert!(matches!(result, Err(Error::InsufficientAllowance(_)))); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move this to just above
macro_rules!
. The rationale is:std
or any official crate likeregex
)