Skip to content
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): add Pausable and Capped extensions for ERC-20 token #49

Closed
wants to merge 23 commits into from

Conversation

bidzyyys
Copy link
Collaborator

@bidzyyys bidzyyys commented Apr 29, 2024

Closes #34 and #42.

PR Checklist

  • Tests
  • Documentation

@bidzyyys bidzyyys self-assigned this Apr 29, 2024
This was linked to issues Apr 29, 2024
@bidzyyys bidzyyys added this to the Release v0.1.0 milestone Apr 29, 2024
Copy link

netlify bot commented Apr 29, 2024

👷 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

Name Link
🔨 Latest commit fb6684b

Copy link

codecov bot commented Apr 29, 2024

The author of this PR, bidzyyys, is not an activated member of this organization on Codecov.
Please activate this user on Codecov to display this PR comment.
Coverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.
Please don't hesitate to email us at [email protected] with any questions.

@bidzyyys bidzyyys force-pushed the bidzyyys/extensions branch from c99e569 to 9a40a2e Compare April 30, 2024 11:10
@bidzyyys bidzyyys force-pushed the bidzyyys/extensions branch from 9a40a2e to fb6684b Compare April 30, 2024 11:15
Copy link
Contributor

@alexfertel alexfertel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass done! 🚀 Looking very nice. It's a super clean implementation.

I'll read the docs and come back with a few more thoughts!

I'll also push a couple of small commits polishing comments when I have the time.

"lib/crypto",
"lib/grip",
"lib/grip-proc",
"examples/erc20",
"proc-tests",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this a file in contracts/tests/? I'm not sure this should be a member of the workspace, but instead just an integration test (or tests).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had an issue with this setup - it was either cyclic dependency or sth with std or panic. Need to check that in incoming days.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yeah, ping me on this, it's really frustrating 😢 I think I've dealt with those issues so I may be helpful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can work on it in person soon ;)

@@ -1,11 +1,14 @@
[workspace]
members = [
"contracts",
"erc20-proc",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be re-exported from contracts::erc20, and then erc20-proc should be a private member of the workspace (i.e., not published).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea.

Comment on lines 69 to +70
) -> 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())
self.erc20.burn_from(account, value).map_err(|e| e.into())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be:

        ) -> Result<(), $crate::erc20::Error> {
            self.erc20.burn_from(account, value)

sol_storage! {
#[derive(Default)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@@ -8,6 +8,7 @@ pub const DEFAULT_DECIMALS: u8 = 18;

sol_storage! {
/// Optional metadata of the ERC-20 standard.
#[allow(clippy::pub_underscore_fields)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this a crate-wide lint.

Comment on lines +221 to +235
if from.is_zero() {
let max_supply = self.cap();
let supply = self.total_supply() + value;
if supply > max_supply {
return Err(contracts::erc20::Error::ERC20ExceededCap(
contracts::utils::capped::ExceededCap {
increasedSupply: supply,
cap: max_supply,
},
));
}
}

// Call "wrapped" token
self.erc20._update(from, to, value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if from.is_zero() {
let max_supply = self.cap();
let supply = self.total_supply() + value;
if supply > max_supply {
return Err(contracts::erc20::Error::ERC20ExceededCap(
contracts::utils::capped::ExceededCap {
increasedSupply: supply,
cap: max_supply,
},
));
}
}
// Call "wrapped" token
self.erc20._update(from, to, value)
self.erc20._update(from, to, value)
if from.is_zero() {
let max_supply = self.cap();
let supply = self.total_supply();
if supply > max_supply {
return Err(contracts::erc20::Error::ERC20ExceededCap(
contracts::utils::capped::ExceededCap {
increasedSupply: supply,
cap: max_supply,
},
));
}
}

Comment on lines +53 to +79
// Export ERC-20 features
erc20_impl!();

// Export ERC-20 Burnable features
erc20_burnable_impl!();

// Export ERC-20 Capped features
erc20_capped_impl!();

// Export ERC-20 Pausable features
erc20_pausable_impl!();

// Add `mint` feature
fn mint(
&mut self,
account: Address,
value: U256,
) -> Result<(), contracts::erc20::Error> {
if account.is_zero() {
return Err(contracts::erc20::Error::InvalidReceiver(
contracts::erc20::ERC20InvalidReceiver {
receiver: Address::ZERO,
},
));
}
self.erc20._update(Address::ZERO, account, value)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this looks great!


[dev-dependencies]
contracts = { path = "../contracts", features = ["erc20_metadata", "erc20_burnable", "erc20_pausable", "erc20_capped"] }
erc20-proc = { path = "../erc20-proc" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this dependency shouldn't be necessary, and instead, consumers should have access to these through contracts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, these are great. Though they should run on a fork, until we have one that works, this is good enough.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand on your idea with fork pls

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so we should make some of these more advanced checks e2e by deploying the contract to a local fork and communicating with it. We should try to keep unit tests simple and move most of the actual contract behavior checks to integration tests.

This is not a strong opinion though, and until we have a proper local fork that we can use, I think it's fine we do our testing like this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local fork of the test node you mean?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, Arbitrum One once the Stylus upgrade is done. For now,https://stylus-testnet.arbitrum.io/rpc.

Comment on lines +1 to +2
#[cfg(test)]
mod erc20;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this should be contracts/tests/macros.rs and shouldn't need a cfg flag (haven't tested).

Copy link
Member

@qalisander qalisander left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extremely great job so far!
As I pointed before, storage abstraction is a cool approach.
And the derive macro for contracts implementation is promising.
Awesome POC!

The drawbacks as I can see is that code inside of proc macro has linear complexity O(n) from creation of new logic. Like for erc721 and for every other extension new macro with logic should be created :( And maintaining, changing and I suppose security audit of them will be a bit cumbersome.

@@ -28,9 +37,7 @@ macro_rules! erc20_burnable_impl {
&mut self,
value: alloy_primitives::U256,
) -> Result<(), alloc::vec::Vec<u8>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have it as a typed error?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! See #49 (comment)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!


let expanded = quote! {
impl contracts::erc20::IERC20Virtual for #name {
fn _update(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see that IERC20Virtual should have a bit more methods. Seems we should duplicate all of them and wrap call to super object. Otherwise it is pointless for the trait to be Virtual. Since just a single method _update inside can be virtual.

p.s. I think it is a bit of a flaw in the design that leads to code duplication.


let expanded = quote! {
impl contracts::erc20::IERC20Virtual for #name {
fn _update(
Copy link
Member

@qalisander qalisander Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the same as in the previous comment. Shouldn't you wrap all methods inside IERC20Virtual? Other (parent) contract extension can override different method and here you just call base implementation (auto implement within rust trait) instead of forwarding the other (overriden) method to the super contract.


let expanded = quote! {
impl contracts::erc20::IERC20Virtual for #name {
fn _update(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same. Other methods will be silently auto implemented with base

}

#[external]
#[inherit(ERC20, Metadata)]
#[inherit(Metadata)]
Copy link
Member

@qalisander qalisander Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically for POC every #[inherit] would be better to be removed. Since you are trying to bypass stylus sdk limitation for inheritance that exist currently.
I would vote to have this solution not relying on stylus inheritance at all.
May be we would skimp on WASM compiled a bit :D
Wdt?

erc20_pausable_impl!();

// Add `mint` feature
fn mint(
Copy link
Member

@qalisander qalisander Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I can see this is a new method exposed externally.
Method with exactly same name that exists inside of the preceding macros can not be added (rust name collision should occur).
Stylus sdk's #[selector(...)] macro can help it or the method can be overridden..
Curious how can look like from the end user perspective single overriden function from e.g. IERC20Virtual?

Copy link
Collaborator Author

@bidzyyys bidzyyys May 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this method (mint) is not a part of ERC20.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@qalisander qalisander May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not. I'm saying it is the new one. How user can override existing method?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeap

@@ -0,0 +1,16 @@
[package]
name = "erc20-proc"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How rational can it be to have a dedicated proc crate for every contract? Since we're planning to use contracts crate for every contract

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I need to improve the structure.

}
};

TokenStream::from(expanded)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

into can be used

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand on your idea with fork pls

value: alloy_primitives::U256,
) -> Result<(), contracts::erc20::Error> {
// Call "wrapped" token
self.erc20._update(from, to, value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also every macro assumes that erc20 field exists inside. Weak typing, a bit fragile. @alexfertel what do you think how it can be improved?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can try to refactor it to achieve sth like this, but not sure if it is possible.

#[derive(IERC20)]
#[inner_field(name = "erc20")]
struct ERC20Wrapper {
    erc20: ERC20,
}

impl IERC20Virtual for ERC20 {}

/// Default implementation of `IERC20` trait for `ERC20`.
#[external]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically you don't need this #[external] and many other "externals". They're just auto implementing Router trait and needed if there is an #[inherit(..)] macro

// This macro implements ERC20Burnable functions -- `burn` and `burn_from`.
// Expects an `ERC20 erc20` as a field of `Token`.
// Export ERC-20 features
erc20_impl!();
Copy link
Member

@qalisander qalisander May 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concern I found recently. Seems critical. This is the code of expanded #[external] macro:
image
I can not see any methods from _impl!(...) macro in the router trait implementation. Seems like TokenStream at #[external] macros encounters tokens like "_impl!(...)". And nothing is exported. Looks like a function macros has less priority in this case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argh, that's big bummer. We would have found this earlier if we had tests....

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically I'm working on tests. It was matter of time to find it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yep. Kinda priorities should be making working and compiling code first, then polishing documentation and codestyle

@alexfertel
Copy link
Contributor

@bidzyyys, per our discussion during the retreat, should we close this one for now, and if we decide to revisit this approach, we can reopen the PR?

@bidzyyys
Copy link
Collaborator Author

@bidzyyys, per our discussion during the retreat, should we close this one for now, and if we decide to revisit this approach, we can reopen the PR?

Yes, we need to close it, but let's keep the branch for the future.

@bidzyyys bidzyyys closed this May 15, 2024
@alexfertel alexfertel removed this from the Release v0.1.0 milestone Jun 1, 2024
@alexfertel alexfertel deleted the bidzyyys/extensions branch June 21, 2024 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

ERC-20 Capped ERC-20 Pausable
3 participants