-
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): add Pausable and Capped extensions for ERC-20 token #49
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 →
|
The author of this PR, bidzyyys, is not an activated member of this organization on Codecov. |
c99e569
to
9a40a2e
Compare
9a40a2e
to
fb6684b
Compare
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.
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", |
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.
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).
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 had an issue with this setup - it was either cyclic dependency or sth with std
or panic
. Need to check that in incoming days.
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.
Oh, yeah, ping me on this, it's really frustrating 😢 I think I've dealt with those issues so I may be helpful.
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 can work on it in person soon ;)
@@ -1,11 +1,14 @@ | |||
[workspace] | |||
members = [ | |||
"contracts", | |||
"erc20-proc", |
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 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).
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 like the idea.
) -> 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()) |
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 can be:
) -> Result<(), $crate::erc20::Error> {
self.erc20.burn_from(account, value)
sol_storage! { | ||
#[derive(Default)] |
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.
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)] |
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 make this a crate-wide lint.
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) |
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.
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, | |
}, | |
)); | |
} | |
} |
// 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) | ||
} |
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, this looks great!
|
||
[dev-dependencies] | ||
contracts = { path = "../contracts", features = ["erc20_metadata", "erc20_burnable", "erc20_pausable", "erc20_capped"] } | ||
erc20-proc = { path = "../erc20-proc" } |
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, this dependency shouldn't be necessary, and instead, consumers should have access to these through contracts
.
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.
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, these are great. Though they should run on a fork, until we have one that works, this is good enough.
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.
Can you expand on your idea with fork pls
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.
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.
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.
local fork of the test node you mean?
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.
Ideally, Arbitrum One once the Stylus upgrade is done. For now,https://stylus-testnet.arbitrum.io/rpc
.
#[cfg(test)] | ||
mod 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.
yeah, this should be contracts/tests/macros.rs
and shouldn't need a cfg
flag (haven't tested).
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.
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>> { |
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.
Is it possible to have it as a typed error?
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! See #49 (comment)
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.
Nice catch!
|
||
let expanded = quote! { | ||
impl contracts::erc20::IERC20Virtual for #name { | ||
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 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( |
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.
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( |
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.
Same. Other methods will be silently auto implemented with base
} | ||
|
||
#[external] | ||
#[inherit(ERC20, Metadata)] | ||
#[inherit(Metadata)] |
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.
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( |
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.
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?
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.
But this method (mint
) is not a part of 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.
➕
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.
It is not. I'm saying it is the new one. How user can override existing method?
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.
Yeap
@@ -0,0 +1,16 @@ | |||
[package] | |||
name = "erc20-proc" |
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.
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
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 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, I need to improve the structure.
} | ||
}; | ||
|
||
TokenStream::from(expanded) |
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.
into
can be used
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.
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) |
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.
Also every macro assumes that erc20
field exists inside. Weak typing, a bit fragile. @alexfertel what do you think how it can be improved?
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.
Not sure yet.
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 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] |
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.
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!(); |
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.
One concern I found recently. Seems critical. This is the code of expanded #[external] macro:
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.
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.
argh, that's big bummer. We would have found this earlier if we had 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.
Basically I'm working on tests. It was matter of time to find it
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.
But yep. Kinda priorities should be making working and compiling code first, then polishing documentation and codestyle
@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. |
Closes #34 and #42.
PR Checklist