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

Add a governor extension that implements a proposal guardian #5303

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

Amxx
Copy link
Collaborator

@Amxx Amxx commented Nov 19, 2024

Fixes #5301

PR Checklist

  • Tests
  • Documentation
  • Changeset entry (run npx changeset add)

@Amxx Amxx added this to the 5.3 milestone Nov 19, 2024
Copy link

changeset-bot bot commented Nov 19, 2024

⚠️ No Changeset found

Latest commit: e56c8b2

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@Amxx Amxx requested a review from arr00 November 19, 2024 10:23
@arr00
Copy link
Contributor

arr00 commented Nov 20, 2024

#5301

@arr00
Copy link
Contributor

arr00 commented Nov 20, 2024

Looks good other than some comments. Would it make sense to call it something like GovernorCancelCouncil to be more explicit? I could imagine daos having multiple different "security" related councils.

@arr00
Copy link
Contributor

arr00 commented Nov 25, 2024

Compound Governance already has a feature like this where the council is called the proposalGuardian. If we aren't too against the wording we could reuse it.

https://github.com/compound-finance/compound-governance/blob/15614c913d548c7a7a4a3f3543069562d120eb7d/contracts/GovernorBravoDelegate.sol#L768-L784

@arr00 arr00 force-pushed the feature/governor/security_council branch from 9bc440d to d613cc8 Compare December 13, 2024 18:21
@arr00 arr00 changed the title Add a governor extension that implements a security council Add a governor extension that implements a proposal guardian Dec 13, 2024
@arr00 arr00 requested a review from ernestognw December 18, 2024 17:48
Copy link
Member

@ernestognw ernestognw left a comment

Choose a reason for hiding this comment

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

What's the rationale to allow a proposer to cancel a proposal at any time?

I think the contract would be simpler if it just focus on allowing the guardian to cancel at any time and otherwise fallback to their original behavior with super.

Comment on lines +32 to +38
// if there is no proposal guardian
// ... only the proposer can cancel
// ... no restriction on when the proposer can cancel
uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash);
address proposer = proposalProposer(proposalId);
if (caller != proposer) revert GovernorOnlyProposer(caller);
return _cancel(targets, values, calldatas, descriptionHash);
Copy link
Member

Choose a reason for hiding this comment

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

The default behavior in Governor is to only allow the proposer to cancel when the proposal is still pending. I don't think we want to allow proposers to cancel after that.

Also, if we want to keep the default behavior, wouldn't it be better to just?:

Suggested change
// if there is no proposal guardian
// ... only the proposer can cancel
// ... no restriction on when the proposer can cancel
uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash);
address proposer = proposalProposer(proposalId);
if (caller != proposer) revert GovernorOnlyProposer(caller);
return _cancel(targets, values, calldatas, descriptionHash);
return super.cancel(targets, values, calldatas, descriptionHash);

This would simplify this function imo

Copy link
Collaborator Author

@Amxx Amxx Jan 9, 2025

Choose a reason for hiding this comment

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

return super.cancel(targets, values, calldatas, descriptionHash); is already what we do in the last case ... so what you are proposing would be simplified further by doing

if (guardian == caller) {
    // if there is a proposal guardian, and the caller is the proposal guardian
    // ... just cancel
    return _cancel(targets, values, calldatas, descriptionHash);
} else {
    // The caller is not the proposal guardian
    // ... apply default behavior
    return super.cancel(targets, values, calldatas, descriptionHash);
}            

Note that it would change the behavior! What we have right now is:

  • if there is no guardian, the proposer can cancel at any point
  • if there is a guardian, the guardian can cancel at any point
  • if there is a guardian, the proposer can only cancel during the propose phase (before vote starts)

The changes would make it that if there is no guardian, the proposer is still restricted to only canceling during the propose phase.

What behavior do we want ?

Copy link
Member

Choose a reason for hiding this comment

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

I shared a proposal down below. My main concern is that we're missing supper in the branches where there's no guardian and when there's a guardian.

For example, in this code, users could override _validateCancel and call super to 1) add the guardian behavior and 2) extend the canceler permissions:

function _validateCancel(...) internal virtual {
    // Current logic
    uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
    _validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Pending));
    if (_msgSender() != proposalProposer(proposalId)) {
        revert GovernorOnlyProposer(_msgSender());
    }
}

...


function cancel(...) public virtual returns (uint256) {
  _validateCancel();
  return _cancel(targets, values, calldatas, descriptionHash);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

In this solution, we will not call super for _validateCancel, which may also skip side effects. I don't see how it's materially better and I'm weary of adding another internal function.

Copy link
Member

Choose a reason for hiding this comment

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

_validateCancel could be view though

@arr00
Copy link
Contributor

arr00 commented Dec 23, 2024

What's the rationale to allow a proposer to cancel a proposal at any time?

I think the contract would be simpler if it just focus on allowing the guardian to cancel at any time and otherwise fallback to their original behavior with super.

The inspiration for this PR is in #5260 which was then replaced by #5301. There is some context missing but will try to give a TLDR.

  • The assumption that governance correctly identifies all faulty proposals is flawed.
    • There are times that a proposal needs to be created strategically but then need to be cancelled.
    • Proposals very often have to be cancelled after the pending period (see compound governance)
  • Compound governance allows cancellation by proposers at any point which is what Enable proposal cancelation by proposer after pending period #5260 advocated for
  • @Amxx pointed out that this gives a possibly exploitive permission to the proposer over the community as they could manipulate governance by cancelling at any point.
    • Say governance wants to do A but proposer X wants A to be delayed by a week. Proposer X would frontrun another proposer to propose A so other community members wouldn't propose. Proposer X cancels proposal A right before execution, requiring a whole new proposal process.
  • While I don't know of any occurrences of the above, the possibility makes it preferred that the cancellation comes from a trusted community proposal guardian (hopefully a multisig)
    • If governance does not actually set the cancel guardian, it is definitely preferred to pass this authority over to the proposers.

@ernestognw
Copy link
Member

ernestognw commented Jan 3, 2025

@Amxx pointed out that this gives a possibly exploitive permission to the proposer over the community as they could manipulate governance by cancelling at any point.

Right, this was my interpretation as well. I also don't have any concrete example of such an exploit.

Getting back to the whole idea of enhancing cancellation capabilities, one concern I have with this design is the pattern:

if (...)  {
  ...
  _cancel(...);
} else if (...) {
  ...
  _cancel(...);
} else {
  super.cancel();
}

The reasoning is that we're allowing to bypass cancel side effects. By not calling super in certain situations (i.e. whether the caller is the proposer or the guardian). This is why I wanted to reduce the branches as much as possible

Consider a contract that inherits from GovernorProposalGuardian, but also inherits from an abstract contract that counts how many cancellations have been made (e.g. GovernorCancellationCounter), so that:

contract GovernorCancellationCounter {
  uint256 cancellations;

  function cancel(...) public override virtual returns (uint256) {
      cancellations++;
      super.cancel(...);
    }
}

contract MyGovernor is ..., GovernorCancellationCounter, GovernorProposalGuardian { ... } // Order is important

In this case, the count would be missing the internal branches that don't call super. Although it's easier to override _cancel, I would say there may be legitimate reasons to prefer cancel.

I'd feel more comfortable if Governor implements an internal:

function _validateCancel(...) internal virtual {
    // Current logic
    uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
    _validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Pending));
    if (_msgSender() != proposalProposer(proposalId)) {
        revert GovernorOnlyProposer(_msgSender());
    }
}

...


function cancel(...) public virtual returns (uint256) {
  _validateCancel();
  return _cancel(targets, values, calldatas, descriptionHash);
}

I think this way users can override _validateCancel and we can use it in GovernorProposalGuardian to implement the newer branches without missing the super.cancel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Governor cancel guardian feature
3 participants