-
Notifications
You must be signed in to change notification settings - Fork 329
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
Erc721 votes and general Votes component #1114
base: main
Are you sure you want to change the base?
Conversation
Hey @ggonzalez94. I have some small comments, but since the design can still change, I won't submit those to avoid unnecessary noise. I will leave my thoughts on this comment: TLDR: Using traits to pass custom functionality to component implementations is a powerful feature, that we've been using to provide some capabilities hard to implement otherwise, like Hooks for token transfers. With this said, abusing this feature is an antipattern IMO, since users of the library need to define/import extra code that is called in a different context, which can be both confusing and easy to misuse. In this context, requiring an extra implementation ( Regarding the proposed design, I'm concerned about the UX for users of the library. The flow looks like this: Current design:
This can get quite verbose and somehow not as simple as it could be, I think we should favor having two separate components even if it includes some code repetition, just to improve the UX, since the user would only need to:
With this said, I think we should be able to reuse code in a separate module (not a component), where we can have the common storage and the common embeddable implementation defined using I recommend we separate the components now even if we have to reuse the Storage struct and the common logic in the embeddable implementation. PD: When proposed to use two different implementations of the same component, I was thinking of something like Ownable, where the only thing that needs to change in the contract is the embedded implementations |
Hey @ericnordelo! Thanks for the detailed feedback. I see your point, so I've been playing a bit with this and I don't think the DEX is that different between two different components vs single component with two different #[starknet::contract]
pub mod ERC721VotesMock {
component!(path: VotesComponent, storage: erc721_votes, event: ERC721VotesEvent);
component!(path: ERC721Component, storage: erc721, event: ERC721Event);
component!(path: SRC5Component, storage: src5, event: SRC5Event);
component!(path: NoncesComponent, storage: nonces, event: NoncesEvent);
//Votes and ERC721Votes
// Here's the only major difference - where instead of having two `impl`(internal and external)
// We have three(common internal and external) + ERC721 specific
#[abi(embed_v0)]
impl VotesImpl = VotesComponent::VotesImpl<ContractState>;
impl VotesInternalImpl = VotesComponent::InternalImpl<ContractState>;
impl ERC721VotesImpl = VotesComponent::ERC721VotesImpl<ContractState>;
// ERC721
#[abi(embed_v0)]
impl ERC721MixinImpl = ERC721Component::ERC721MixinImpl<ContractState>;
impl ERC721InternalImpl = ERC721Component::InternalImpl<ContractState>;
// Nonces
#[abi(embed_v0)]
impl NoncesImpl = NoncesComponent::NoncesImpl<ContractState>;
#[storage]
struct Storage {
#[substorage(v0)]
erc721_votes: VotesComponent::Storage,
#[substorage(v0)]
erc721: ERC721Component::Storage,
#[substorage(v0)]
src5: SRC5Component::Storage,
#[substorage(v0)]
nonces: NoncesComponent::Storage
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
ERC721VotesEvent: VotesComponent::Event,
#[flat]
ERC721Event: ERC721Component::Event,
#[flat]
SRC5Event: SRC5Component::Event,
#[flat]
NoncesEvent: NoncesComponent::Event
}
/// Required for hash computation.
pub(crate) impl SNIP12MetadataImpl of SNIP12Metadata {
fn name() -> felt252 {
'DAPP_NAME'
}
fn version() -> felt252 {
'DAPP_VERSION'
}
}
//
// Hooks and ctor not included to make the example shorter
//
} As you can see the only big difference vs how this looks on a contract that uses ERC20Votes is only one |
My main concern was the extra impl, because while it is true we want to avoid code repetition, I'm hesitant regarding adding trait requirements in component impls, when the implementation of the trait doesn't need to be modified by the user. With this said, I realized now that with the approach you propose users won't even need to explicitly add the extra ERC721VotesImpl in the contract, because as you mentioned, one implementation will depend on the ERC721Component, and the other one on the ERC20Component, then the compiler can automatically get the right impl as long as the parent module (VotesComponent) is in scope. The preset can look like this:
Notice that it is enough to add just the VotesImpl as long as VotesComponent is in scope. And an ERC20Votes preset would only need to change the ERC721Component, the VotesImpl would work out of the box if the ERC20Votes impl is defined correctly in the VotesComponent. I have nothing against this approach of one component then. Let's add the ERC20Votes impl to the PR too. Even if we are not removing the Legacy ERC20Votes yet, we can mark it in the CHANGELOG as deprecated, and we can start favoring the new VotesComponent. |
I think the overall design looks really good! Some notes:
Great point
One case to consider is if a contract wanted to declare both ERC20 and ERC721 components. The contract can't expose both interfaces, but they could have the both InternalImpls in scope. The current design can't support that, but maybe we could leverage negative traits in the token votes impls? This was briefly discussed offline, but I'll repeat it here: I think we should remove the
|
@@ -35,7 +35,6 @@ pub trait IVotes<TState> { | |||
} | |||
|
|||
/// Common interface for tokens used for voting(e.g. `ERC721Votes` or `ERC20Votes`) |
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 update accordingly, or remove the comment since the user shouldn't implement this trait.
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.
You mean removing the trait or removing the comment or making it just a code comment and not ///
? I think having the trait is valuable since it forces contracts that use the VotesComponent
to have an implementation of this trait and it allows someone that wants to use their own token voting mechanism to implement 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.
I'd adjust the comment because it's not an interface. This is an important distinction because trait and interface definitions look (unfortunately) similar. I might even consider moving this away from interface.cairo
and into the Votes file or mod to make it clear this isn't meant to be exposed
Now that we settled on the design, I cleaned up the component and added a bunch of tests. I still need to finish the tests, add documentation and implement checkpoints using Vec(so far I'm reusing the current implementation that uses StorageArray). |
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.
Looking good, sir! I left some comments and questions
@@ -35,7 +35,6 @@ pub trait IVotes<TState> { | |||
} | |||
|
|||
/// Common interface for tokens used for voting(e.g. `ERC721Votes` or `ERC20Votes`) |
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'd adjust the comment because it's not an interface. This is an important distinction because trait and interface definitions look (unfortunately) similar. I might even consider moving this away from interface.cairo
and into the Votes file or mod to make it clear this isn't meant to be exposed
let state = setup_erc721_votes(); | ||
|
||
assert_eq!(state.get_voting_units(OWNER()), 10); | ||
assert_eq!(state.get_voting_units(RECIPIENT()), 0); |
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 nit: I'd change RECIPIENT
to OTHER
or something like that and only use RECIPIENT
for cases where the account is the actual recipient of a tx
break; | ||
} | ||
mock_state.erc721.mint(OWNER(), i); | ||
// We manually transfer voting units here, since this is usually implemented in the hook |
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 don't we integrate the logic in the mock contract hook?
start_cheat_caller_address(contract_address, OWNER()); | ||
|
||
state.delegate(RECIPIENT()); | ||
spy.assert_event_delegate_changed(contract_address, OWNER(), ZERO(), RECIPIENT()); | ||
spy.assert_only_event_delegate_votes_changed(contract_address, RECIPIENT(), 0, 10); | ||
assert_eq!(state.get_votes(RECIPIENT()), 10); | ||
assert_eq!(state.get_votes(OWNER()), 0); |
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 we can improve the readability of the tests by using variables that better describe what they represent. For example, delegator
, delegatee
, and total_votes
instead of OWNER
, RECIPIENT
, and 10
. This is especially helpful for longer test cases with event assertions. Forgive the nitpickiness 😅 I know this is a WIP
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1114 +/- ##
==========================================
- Coverage 88.87% 88.62% -0.25%
==========================================
Files 57 58 +1
Lines 1375 1372 -3
==========================================
- Hits 1222 1216 -6
- Misses 153 156 +3
... and 1 file with indirect coverage changes Continue to review full report in Codecov by Sentry.
|
Fixes #984
This PR introduces a generic
Votes
component that contains reusable logic for other token voting mechanisms. When we implemented ERC20Votes we did it in a single component that used storage variable names specific to ERC20 and it also usesStorageArray
behind the scenes(Vec
wasn't available at that moment), so the design is suboptimal and we would like to move away from it at least forERC721Votes
and future voting contracts.This PR contains:
Votes
component that holds all the common logic for voting.impl
inside that component that has logic specific toERC721Votes
andERC20Votes
. If we want to add another form of voting we just need to add a newimpl
block to the same component that implements theget_voting_units
function from theTokenVotesTrait
trait.governance
module.ERC20Votes
.Things that are still missing
Vec
instead ofStorageArray
. This is being challenging given thatVec
is built to work with Storage and not regular structsPR Checklist