OpenZeppelin / cairo-contracts

OpenZeppelin Contracts written in Cairo for Starknet, a decentralized ZK Rollup
https://docs.openzeppelin.com/contracts-cairo
MIT License
802 stars 323 forks source link

dev: two step ownable #809

Closed milancermak closed 5 months ago

milancermak commented 8 months ago

Fixes #304

Adds support for two step ownership transfer, i.e. a new owner is first proposed by the current owner by calling transfer_ownership, then the proposed owner has to call accept_ownership to become new owner. Modeled after the Solidity version as linked in the original issue.

I kept the code in the ownable.cairo component because of code reuse. While I'm not a DRY fanatic, in this case I think it works pretty well. From a user's perspective, if they have a contract with (one step) Ownable and decide to upgrade to two step Ownable, they only have to switch from

 #[abi(embed_v0)]
 impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;

to

#[abi(embed_v0)]
impl OwnableTwoStep = OwnableComponent::OwnableTwoStepImpl<ContractState>;

so it's a one line diff. Everything else (component! declaration and whatnot) stays the same and the contract upgrade is safe (current owner is not lost) as the underlying storage is the same.

The one slight drawback is that in the component impl, since both interfaces have functions of the same signature, I had to use the impl name to fully qualify which one should be run. In practice, it means going from self.transfer_ownership(newOwner) to Ownable::transfer_ownership(ref self, newOwner); in two or three places. It's a bit ugly IMO since it's inconsistent with the surrounding code, but because it's hidden inside the code of the component, I think it's a good trade-off. Curious to hear your thoughts.

PR Checklist

milancermak commented 8 months ago

Seeking initial feedback on the proposed approach.

The whole test suite is missing, I'll add it once we flesh out the rest.

On more point I'd like to discuss - I always felt like there should be a way for the pending owner to renounce their nomination. This isn't present in the Solidity version (not sure if it was considered). I don't have a use case for it, just that it "feels fair" for the pending owner 😄 WDYT?

martriay commented 8 months ago

Thanks for pushing this. Overall direction looks really good to me! I appreciate the effort to streamline migration for existing Ownable users.

I kept the code in the ownable.cairo component because of code reuse.

I don't think it's just code reuse, I still believe it makes sense since this is to me just an alternative implementation of the same Ownable component. One drawback though is that this forces an extra storage element for vanilla Ownable users. It'd be useful to understand what's the cost difference (if any).

It's a bit ugly IMO since it's inconsistent with the surrounding code, but because it's hidden inside the code of the component, I think it's a good trade-off.

Agree on containing any ugliness within the component, away from the user interface. I presume using self results in a compilation error?

I always felt like there should be a way for the pending owner to renounce their nomination.

I honestly don't see the benefit, my bias is always towards minimizing complexity.

milancermak commented 8 months ago

One drawback though is that this forces an extra storage element for vanilla Ownable users. It'd be useful to understand what's the cost difference (if any).

My understanding is that there would only be a minimal increased cost when declaring a class, because of the increased bytecode size. I haven't verified it though. Otherwise, during runtime, there should be no difference whatsoever since the Ownable_pending_owner storage slot is not used (with the one step version).

I presume using self results in a compilation error?

Yes, something like error: Ambiguous method call. More than one applicable trait function with a suitable self type was found: IOwnable::transfer_ownership and IOwnableTwoStep::transfer_ownership. Consider adding type annotations or explicitly refer to the impl function.

my bias is always towards minimizing complexity.

That's a good point as the alternative would require another public function 👍

Will continue with adding tests and cleaning the PR up then.

milancermak commented 8 months ago

Code-wise the PR is ready for review (so I'm marking it as such).

However I haven't yet touched the docs section. How do you want to handle it? Should the two step process be a subsection under "Ownership and Ownable" or should it be its own section?

Should I test the component on testnet as well?

milancermak commented 8 months ago

Oh, forgot to mention - in the test file (test_ownable_twostep.cairo), I didn't test the internal functions that are already covered in test_ownable.cairo. Some tests are only copy-pasted (e.g. the whole section for renounce_ownership) since the functionality is the same in both versions. Finally, I added two more complex tests (test_full_two_step_transfer and test_pending_accept_after_owner_renounce), halfway between a unit test and a integration test 😅 Let me know if you want to keep or delete them.

martriay commented 8 months ago

However I haven't yet touched the docs section. How do you want to handle it? Should the two step process be a subsection under "Ownership and Ownable" or should it be its own section?

I'd add a new section, while documenting the component in the API section.

Should I test the component on testnet as well?

If you can do that, it'd be great. But to be fair we haven't been doing so since we're usually running behind the latest Cairo language which is not always deployed.

Thanks for the work put into this, and please bear with us while we make some space to properly review it.

milancermak commented 8 months ago

Dealt with all the points from the review. Thanks for the keen eye @ericnordelo.

Also did try the functionality on testnet. It works as designed. Even upgrading from one-step to two-step ownable (and back) is without any surprises.

Will proceed with writing the docs then.

milancermak commented 8 months ago

I forgot to do pendingOwner. Should I add it? On one hand, it's good for consistency, on the other, Camel is there for backwards compatibility and Cairo 1 is supposed to be snake_only and since two-step transfer wasn't there for Cairo 0, nothing breaks (I think). Thoughts?

milancermak commented 8 months ago

gm guys

Just pushed the docs - added both a section to the general overview and also updated the API docs.

Three things to point out:

1) I've copy-pasted the Usage section of TwoStepOwnable from Ownable, just changed the appropriate lines in code, is that alright? 2) Not quite sure what to put under the Interface section - note the TODO. 3) I've also added an "Upgrading from Ownable" subsection, as I think it might be helpful for users, but it renders poorly in the side menu 🙈

Thanks for any feedback 🚀

martriay commented 8 months ago

I forgot to do pendingOwner.

we already have acceptOwnership so we need to be consistent: we either have them both, or none. No strong opinion, either is fine

milancermak commented 7 months ago

A valid point that two-step ownable is not a separate component. I've updated the docs based on the comments above, hopefully satisfactory. If not, feel free to unresolve the conversation or post new comments.

However I now feel that the docs don't show well enough how to use the two step functionality of the component. An easy copy-paste-able snippet like this would help IMO:

#[abi(embed_v0)]
impl OwnableTwoStepImpl = OwnableComponent::OwnableTwoStepImpl<ContractState>;

WDYT?

Also the mention about upgrading is now gone. Not sure how important that is.

milancermak commented 7 months ago

Rebased from main to get the CHANGELOG and added two entries there.

Also added the snippet into the docs pages as discussed above.

milancermak commented 7 months ago

Whoops, sorry about the force-push, habit 🙈

milancermak commented 7 months ago

Just a quick note - haven't abandoned this, just have to deal with some other stuff. I'm aiming to finish this PR this weekend.

martriay commented 7 months ago

thanks for the heads up!

martriay commented 7 months ago

@milancermak is this ready for final review?

milancermak commented 7 months ago

Yes

milancermak commented 6 months ago

GM sirs and happy new year. Just bumping this, I've addressed the latest comments. Let me know if there's anything else missing.

milancermak commented 5 months ago

Resurfacing this PR 😇

martriay commented 5 months ago

Thanks for the bump! This was already in my backlog for this week :)

image

milancermak commented 5 months ago

I already did the onchain testing some time ago - can't find the TXs anymore. But basically I did a super simple contract that was Ownable and Upgradable, did the upgrade back and forth, verified that the owner stayed the same during upgrades and that the new owner was transferred successfully after it was accepted and that a protected function could only be access by the owner. It worked smoothly.

milancermak commented 5 months ago

Should be ready for final review and merge @martriay @ericnordelo

milancermak commented 5 months ago

Done. Thanks @martriay

martriay commented 5 months ago

Thank you, for the contribution and the patience. Just requested @ericnordelo's approval (because he requested changes) to unblock the merge.