openzeppelin-contracts icon indicating copy to clipboard operation
openzeppelin-contracts copied to clipboard

Update hashProposal visibility to view

Open 0xDiscotech opened this issue 1 year ago • 9 comments

Fixes

Update hashProposal() function visibility to view instead of pure to allow an override using block.chainid and/or address(this) in order to guarantee more uniqueness on the same proposal but on different networks.

Can be very useful when the proposal is used as a kind of proof to validate the vote, and the same proposal is created on different networks. This allows the developer to mitigate a replay attack over the same proposalId in a clean way by overriding the function to read from the environment or the contract's state.

PR Checklist

  • [x] Tests
  • [x] Documentation
  • [x] Changeset entry (run npx changeset add)

0xDiscotech avatar Jun 23 '24 23:06 0xDiscotech

🦋 Changeset detected

Latest commit: 667d07df1bc0a4813a6929c9030b6a0cc0811a04

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openzeppelin-solidity Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

changeset-bot[bot] avatar Jun 23 '24 23:06 changeset-bot[bot]

Thank you @0xDiscotech for this proposal.

I'm not sure what the consequences of changing the proposalId computation would be on UI such as Tally ... but given it is virtual, I don't see why we would allow some changes but not that change.

So its good to me

Amxx avatar Jun 26 '24 08:06 Amxx

@0xDiscotech, we discussed this PR internally, and we have some concern with the consequences of using pure vs view.

Using pure would allow change that make the fonction change value independently of the input parameter (that is the whole point of pure vs view). This means reading state variable, reading the time, reading anything that is not constant.

If this was to happen, then the proposalId may not be consistently computed through the lifecycle of a proposal, and that would be bad. Potentially, it could be used to hide some attack vector that allows a priviledge user to prevent a proposal from being executed. This could be happen by mistake, or intentionally by a malicious dev.

On the other hand, we don't really understand the usecase. If two governors, on the same chain, or on two different chains, are planning to execute the exact same logic, what is the issue using the same identifier for both proposals ? The same proposalId means the same input was proposed. The execution outcome may differ depending on the governor instance doing it, but the input remains the same.

Whe voting on it, the voting scheme (that is based on EIP-721) does already prevent cross-instance / cross-chain replay.

With all that in mind, we see a not very likelly but still very clear risk ... and we don't really see an upside to that.

Amxx avatar Jun 26 '24 16:06 Amxx

Thanks for you response @Amxx @ernestognw!

You have a good point there, but I will explain the reason behind my proposal: The idea of adding it to view came from a project where our team created a Governor extension interacting with Worldcoin's World ID protocol. In this project, we used the proposalId as a key part of the input to verify that a user is a unique human. Since Worldcoin preserves user information and only verifies if the user is human, it always returns the same nullifierHash for the input action. It makes sense to use the proposalId as input and store that nullifierHash to avoid double-spending by the same user. However, you must ensure that the proposalId is unique to prevent replay attacks.

In our solution, we had to concatenate the description with a kind of 'signature' using the chain ID and the Governor's address. This breaks the functionality of the hashProposal() function because it won't return the exact same proposalId after calling propose(). You can see our implementation here: GovernorWorldID.sol. The solution was somewhat 'hacky' because we couldn't add that data inside the hashProposal() function, which would have been the cleanest way.

This approach might be very specific to the mentioned use case, but it's not uncommon for future Governors to use similar protocols to get proof-of-personhood while preserving user privacy.

In summary, my point is that developers can still modify the logic behind proposalId generation. However, restricting this to pure functions makes things more difficult and complex to handle. The benefit of allowing more flexibility in these cases is that it ensures guaranteed uniqueness not only across different chains but also across different DAOs. Currently, the same proposalId can be generated in two different DAOs with the same proposal as input.

0xDiscotech avatar Jun 26 '24 20:06 0xDiscotech

Thanks for you response @Amxx @ernestognw!

The idea of adding it to view came from a project where our team created a Governor extension interacting with Worldcoin's World ID protocol. In this project, we used the proposalId as a key part of the input to verify that a user is a unique human. Since Worldcoin preserves user information and only verifies if the user is human, it always returns the same nullifierHash for the input action. It makes sense to use the proposalId as input and store that nullifierHash to avoid double-spending by the same user. However, you must ensure that the proposalId is unique to prevent replay attacks.

In our solution, we had to concatenate the description with a kind of 'signature' using the chain ID and the Governor's address. This breaks the functionality of the hashProposal() function because it won't return the exact same proposalId after calling propose(). You can see our implementation here: GovernorWorldID.sol. The solution was somewhat 'hacky' because we couldn't add that data inside the hashProposal() function, which would have been the cleanest way.

This approach might be very specific to the mentioned use case, but it's not uncommon for future Governors to use similar protocols to get proof-of-personhood while preserving user privacy.

In summary, my point is that developers can still modify the logic behind proposalId generation. However, restricting this to pure functions makes things more difficult and complex to handle. The benefit of allowing more flexibility in these cases is that it ensures guaranteed uniqueness not only across different chains but also across different DAOs. Currently, the same proposalId can be generated in two different DAOs with the same proposal as input.

Also we could use the address(this) and block.chainid as the default implementation of hashProposal(), but I don't know if it something you are interested in. Maybe I can open a new PR with that change (only in case you agree on moving forward with the pure to view modification).

0xDiscotech avatar Jun 26 '24 21:06 0xDiscotech

Also we could use the address(this) and block.chainid as the default implementation of hashProposal(), but I don't know if it something you are interested in.

This is something that was discussed in the past, and was rejected. We were concerned that changing the hash computation could break tools such as Tally, and we did not see a clear advantage (as discussed above). It would also cause issues if a governor contract is upgraded while proposals are active.

It is not inconceivable that we make the function view, so that overrides can use address(this) ... but for sure we are not going to change the default function.

Amxx avatar Jun 27 '24 07:06 Amxx

Also we could use the address(this) and block.chainid as the default implementation of hashProposal(), but I don't know if it something you are interested in.

This is something that was discussed in the past, and was rejected. We were concerned that changing the hash computation could break tools such as Tally, and we did not see a clear advantage (as discussed above). It would also cause issues if a governor contract is upgraded while proposals are active.

It is not inconceivable that we make the function view, so that overrides can use address(this) ... but for sure we are not going to change the default function.

Awesome, makes sense. Good to know 👍

0xDiscotech avatar Jun 27 '24 14:06 0xDiscotech

The idea of adding it to view came from a project where our team created a Governor extension interacting with Worldcoin's World ID protocol. In this project, we used the proposalId as a key part of the input to verify that a user is a unique human

This is an interesting insight. I guess the problem here is that we never intended proposalId to be something "non-replayable" outside of the context of this contract. The security assumptions here indicate that there shouldn't be an issue, though using the proposalId in the way you describe definitely requires replayability protection.

From your code I see you added some information to the returned proposalId. That's fine in my opinion since it's not altering the internal assumptions the Governor does about the proposalId.

I would be fine making the function view, it's just that most of the lifecycle of the governor assumes that the proposal id function is a pure function (in the sense that it doesn't have any side effect), and allowing to remove such guarantee sounds like a big compromise imo

ernestognw avatar Jun 27 '24 21:06 ernestognw

That's fine in my opinion since it's not altering the internal assumptions the Governor does about the proposalId. Yes, even though that approach works, the off-chain interaction is affected since hashProposal() won't return the same proposalId as _propose().

My point of view is that by setting it to view, you provide more flexibility to cover scenarios requiring more uniqueness. This allows developers to implement it straightforwardly without breaking any off-chain interaction.

That said, you make a good point. While I maintain my perspective, I respect and understand any decision you make 🫡

0xDiscotech avatar Jun 29 '24 22:06 0xDiscotech

Hi @0xDiscotech,

We've been discussing this change since #5290 came up as a requirement for some other DAOs too. Those changes may likely end up being even more aggressive than this visibility change so we might get this merged before.

ernestognw avatar Nov 01 '24 09:11 ernestognw

Hi @0xDiscotech,

We've been discussing this change since #5290 came up as a requirement for some other DAOs too. Those changes may likely end up being even more aggressive than this visibility change so we might get this merged before.

Hi @ernestognw, Thanks for letting me know, this sounds good! Do you need some change from side on the code?

0xDiscotech avatar Nov 01 '24 21:11 0xDiscotech

Hi @ernestognw, Thanks for letting me know, this sounds good! Do you need some change from side on the code?

All good, just an update :D

ernestognw avatar Nov 04 '24 07:11 ernestognw

Just wanted to leave an update here--we are currently leaning towards just merging #5290 which moves in the direction of deprecating hashProposal eventually in favor of a view function named getProposalId.

arr00 avatar Nov 22 '24 19:11 arr00

Closing in favor of #5290.

Amxx avatar Dec 11 '24 13:12 Amxx