-
Notifications
You must be signed in to change notification settings - Fork 24
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
Conversation
👷 Deploy request for contracts-stylus pending review.A Netlify team Owner will need to approve the deploy before you can run your build. Are you a team Owner? Visit the deploys page to approve it → Need more help? Learn more in the Netlify docs →
|
388ec26
to
af75199
Compare
The author of this PR, bidzyyys, is not an activated member of this organization on Codecov. |
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.
Looks beautiful! Super clean and as expected 👍
The only changes we need before merging this are regarding Transfer events. Otherwise looks good!
/// [`Error::Overflow`] is returned. | ||
/// If the `from` address doesn't have enough tokens, then the error | ||
/// [`Error::InsufficientBalance`] is returned. | ||
pub fn _update( |
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.
The reason why I didn't include this function in the base implementation is that it is pointless right now: we don't have a way to override internal methods.
I say we keep it for now.
Do you think there's a better name for it? I can't think of anything other than _transfer_impl
and I don't like it.
pub fn _update( | |
pub fn _update( |
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.
I see your point why you had not included _update
at first, but I introduced it to be used also in _burn
mechanism. IMHO it is better to reuse as much code as possible instead of a need for changing the same in many places.
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.
That is why _update
is better IMHO
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.
Right, but note that the above breaks when we need to optimize for performance as well.
fn _transfer(
&mut self,
from: Address,
to: Address,
value: U256,
) -> Result<(), Error> {
if from == Address::ZERO {
...
}
if to == Address::ZERO {
...
}
self._update(from, to, value)?;
Ok(())
}
pub fn _update(
&mut self,
from: Address,
to: Address,
value: U256,
) -> Result<(), Error> {
if from.is_zero() {
...
}
if to.is_zero() {
...
}
}
In the above, you can see that we are checking from.is_zero()
, when we already checked that in _transfer
. This is inefficient. _update
improves code reusability, but it is also more costly to use.
This is only an example, because the only benefit of _update
is not code reuse, but the flexibility it brings when extending the base ERC20 implementation, and that's why I think we should keep it.
Not including it in the base implementation was something totally on purpose: I was trying to strike a good balance between efficiency, code reuse, readability, etc. I still have my reservations about it, but let's see what the Stylus team does with respect to overriding internal methods.
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.
I see your point, need to think how to solve it.
/// [`Error::Overflow`] is returned. | ||
/// If the `from` address doesn't have enough tokens, then the error | ||
/// [`Error::InsufficientBalance`] is returned. | ||
pub fn _update( |
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.
We should say that it emits a Transfer
event. And also add it to all the functions in this call stack.
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.
And also stop emitting the event in transfer
.
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.
Good catch, thank you 👍
contracts/src/erc20/mod.rs
Outdated
if from.is_zero() { | ||
// Mint operation. | ||
// Overflow check required: | ||
// The rest of the code assumes that _total_supply never overflows |
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.
// The rest of the code assumes that _total_supply never overflows | |
// The rest of the code assumes that `_total_supply` never overflows. |
|
||
// Mint some tokens for Alice. | ||
let two = U256::from(2); | ||
contract._balances.setter(alice).set(two); | ||
contract._update(Address::ZERO, alice, two).unwrap(); |
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.
This change makes the test now handle testing both transfer_from
and _update
. We should add separate tests for _update
and keep other tests as isolated as possible. (Note we cannot avoid the actual call to _update
that happens inside transfer_from
.)
wdyt?
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.
I see your point, it's valid. From my perspective, it is better to test if _update
works as expected and than assume in tests that it works -> use _update.unwrap()
.
Touching every time the memory may be painful... Imagine a situation when due to some reasons _balances
field gets renamed. After that change you would need to rename it in 1xxxxx places. While using _update
you would need to adjust only _update
function and the rest would work.
I am open to negotiation on that point @alexfertel @qalisander.
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.
That's fair. We should add tests for _update()
and that's it on my side 👍
(Note that I don't think we'll change _balances
at all, but it's a valid point)
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.
I can add tests to _update()
, let's agree first how we should handle this (discussion in one of the above comments)
contracts/src/erc20/mod.rs
Outdated
/// Indicates a failure where an overflow error in math occurred. Used in | ||
/// `_update`. | ||
Overflow(ERC20Overflow), |
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.
This is great thinking, though I am unsure of the implications. We should talk to the Solidity team about this and see what they think. I'll ask around.
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.
Yes, and the rest of Rust teams. In general I am a fan of making it visible to developers and handling it as quick as possible instead of relying on built-in mechanisms that may change.
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.
Small suggestion to name it ArithmeticOverflow
? Feel free to ignore
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.
I was expecting here some ideas 😅
|
||
sol_storage! { | ||
pub struct ERC20Burnable { | ||
ERC20 erc20; |
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.
The issue I see here is that we don't have C3 linearization yet in the SDK. This means that the inheritance may not end as you'd expect, i.e. in this graph, what does storage look like for the User's contract:
It may be good to research this and make sure all of this works: that's why I was trying to add examples, so that we can test these hierarchies. I'll try to find a solution ASAP when I have some time.
I think it is fine to merge this as is rn, but we need to keep in mind that this will probably not be it's final form.
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.
Good point on that, I have some ideas for spikes but we need to have more than 1 extension to experiment 😅
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.
Looks good!
Extension structure may be will get changed in future but for now it should be fine
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.
Beautiful!
I'm starting to see a couple of issues with the macros this way:
- How do you control visibility? I see you used
pub(crate)
, but what if I want my contract to bepub
? - Imports are a bit awkward, though I think this is fine, since they won't be using an extension without using our contracts.
- Exporting from the root is a bit weird as well, cause they'd be importing not from their corresponding modules but from the root.
Nonetheless, I think we should keep them and see what happens!
I left a few comments because there's a few clippy lints (we should try to fix those), but nothing complicated. After these are done this should be good to go! 🚀
@@ -0,0 +1,192 @@ | |||
//! Optional Burnable extension of the ERC-20 standard. | |||
|
|||
#[macro_export] |
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:
- Consistency with the way we declare the attributes in the rest of the crate.
- This is preferred in Rust (check any item in
std
or any official crate likeregex
) - It's better to keep attributes close to what they decorate so that we don't have to jump around the code (also it's easy to miss them when they're like this).
/// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
/// 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. | |
/// 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`]. |
/// Requires import of: | ||
/// * alloy_primitives::{Address, U256} |
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.
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
pub(crate) fn burn(&mut self, value: U256) -> Result<(), Error> { | |
self.erc20._burn(msg::sender(), value) | |
} | |
pub fn burn(&mut self, value: alloy_primitives::U256) -> Result<(), contracts::erc20::Error> { | |
self.erc20._burn(stylus_sdk::msg::sender(), value) | |
} |
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 comment
The reason will be displayed to describe this comment to others. Learn more.
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) | |
} | |
pub fn burn_from( | |
&mut self, | |
account: alloy_primitives::Address, | |
value: alloy_primitives::U256, | |
) -> Result<(), contracts::erc20::Error> { | |
self.erc20._spend_allowance(account, stylus_sdk::msg::sender(), value)?; | |
self.erc20._burn(account, value) | |
} |
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.
There is a problem with contracts::erc20::Error
-> we cannot have it this way, while it breaks all imports in tests...
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.
yeah, I feared that...not sure what the solution is 😢
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.
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())
}
/// # Events | ||
/// | ||
/// Emits a [`Transfer`] event. | ||
pub fn _update( |
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 add a # Panics
section to the docs since this function may panic now. (it's a clippy lint)
examples/erc20/src/lib.rs
Outdated
@@ -21,6 +31,10 @@ sol_storage! { | |||
#[external] | |||
#[inherit(ERC20, Metadata)] | |||
impl Token { | |||
// This macro implements ERC20Burnable functions -- `burn` and `burn_from`. | |||
// Uses `erc20` Token's attribute. |
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.
// Uses `erc20` Token's attribute. | |
// Expects an `ERC20 erc20` as a field of `Token`. |
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.
gogogogogo 🚀
Resolves #27
PR Checklist