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

Reject proposals early #668

Merged
merged 14 commits into from
Mar 24, 2022
Merged

Reject proposals early #668

merged 14 commits into from
Mar 24, 2022

Conversation

Callum-A
Copy link
Contributor

Addresses Issue #665

Implemented what I implemented for DAO DAO here for the multisigs. If I have missed any functionality or coverage do say.

  • Proposals are now marked as rejected when no votes reach their threshold.
  • Added tests for proposal struct and unit tests for functionality.

Feedback appreciated.

@CLAassistant
Copy link

CLAassistant commented Feb 22, 2022

CLA assistant check
All committers have signed the CLA.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor

@ueco-jb ueco-jb left a comment

Choose a reason for hiding this comment

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

I'll be back to review actual implementation, commented only obvious stuff so far.

Comment on lines 47 to 52
if status == Status::Open && self.expires.is_expired(block) {
status = Status::Rejected;
}
if status == Status::Open && self.is_rejected(block) {
status = Status::Rejected;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if status == Status::Open && self.expires.is_expired(block) {
status = Status::Rejected;
}
if status == Status::Open && self.is_rejected(block) {
status = Status::Rejected;
}
if status == Status::Open && (self.expires.is_expired(block) || self.is_rejected(block)) {
status = Status::Rejected;
}

Comment on lines 102 to 103
/// returns true iff this proposal is sure to pass (even before expiration if no future
/// sequence of possible votes can cause it to fail)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// returns true iff this proposal is sure to pass (even before expiration if no future
/// sequence of possible votes can cause it to fail)
/// returns true if this proposal is sure to pass (even before expiration, if no future
/// sequence of possible votes could cause it to fail)

Comment on lines 108 to 109
/// As above for the rejected check, used to check if a proposal is
/// already rejected.
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO doc comments shouldn't state "as above`, since "above" part won't be shown by IDE, rust compiled docs etc.

Comment on lines 66 to 71
/// This function returns true if and only if vote_count is greater than the threshold which
/// is calculated.
/// In the case where we use yes votes, this function will return true if and only if the
/// proposal will pass.
/// In the case where we use no votes, this function will return true if and only if the
/// proposal will be rejected regardless of other votes.
Copy link
Contributor

@ueco-jb ueco-jb Feb 22, 2022

Choose a reason for hiding this comment

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

Suggested change
/// This function returns true if and only if vote_count is greater than the threshold which
/// is calculated.
/// In the case where we use yes votes, this function will return true if and only if the
/// proposal will pass.
/// In the case where we use no votes, this function will return true if and only if the
/// proposal will be rejected regardless of other votes.
/// This function returns true if vote_count is greater than the threshold.
/// In the case where we use `yes` votes, this function will return true if the proposal will pass.
/// In the case where we use `no` votes, this function will return true if the proposal will be
/// rejected, regardless of other votes.

Those "if and only if" are redundant, as well as some other stuff..

Copy link
Contributor

@maurolacy maurolacy left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks for the contribution.

contracts/cw3-flex-multisig/src/contract.rs Outdated Show resolved Hide resolved
contracts/cw3-fixed-multisig/src/state.rs Outdated Show resolved Hide resolved
Callum-A and others added 2 commits February 23, 2022 11:35
Co-authored-by: Mauro Lacy <maurolacy@users.noreply.github.com>
Co-authored-by: Mauro Lacy <maurolacy@users.noreply.github.com>
Copy link
Contributor

@ueco-jb ueco-jb left a comment

Choose a reason for hiding this comment

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

lgtm

Copy link
Member

@ethanfrey ethanfrey left a comment

Choose a reason for hiding this comment

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

Is rejected doesn't work if the threshold deviates from 50%.

Added suggestions how to fix code and tests

match self.threshold {
Threshold::AbsoluteCount {
weight: weight_needed,
} => self.votes.yes >= weight_needed,
} => vote_count >= weight_needed,
Copy link
Member

Choose a reason for hiding this comment

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

This check doesn't work properly of no votes. I would implement it sepately rather than combining.

Assume a threshold of 67%.

If we have 40% yes and 40% no, it would return false for both.
However, this could be safely marked as rejected.

Your logic works fine when the threshold is 50% (same for both).

I would propose not renaming the function and using it for both cases (as done here), but rather copying is_passed to is_rejected and make the various logic changes there needed (often 1-threshold or such). This is different for each branch and hard to generalise. Better to have two separate functions than combine them

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're completely right, will try and address this today

let info = mock_info(VOTER4, &[]);
let res = execute(deps.as_mut(), mock_env(), info, no_vote).unwrap();

// Verify it is now rejected due to reaching threshold
Copy link
Member

Choose a reason for hiding this comment

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

This is actually a test showing the code is broken.

setup_test_case adds 6 voters with a total weight of 16.
This test cases constructs an example with the odd configuration of 3 votes to reach threshold.

4 no votes should not mark it as rejected. You should require 14 no votes to ensure it is rejected.

I would propose a separate test function to cover this.

#[test]
fn proposal_rejected_quorum() {
let quorum = Threshold::ThresholdQuorum {
threshold: Decimal::percent(50),
Copy link
Member

Choose a reason for hiding this comment

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

Let's use threshold = Decimal::percent(60) so we can test the real case. Then fix the logic to match

Copy link
Member

@ethanfrey ethanfrey left a comment

Choose a reason for hiding this comment

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

Nice fix.
Just request two minor changes, which don't affect correctness

.add_attribute("status", "Open")
);

// Voter 2 votes no, weight 2, total weight for no so far 15, need 14.
Copy link
Member

Choose a reason for hiding this comment

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

👍

Threshold::AbsoluteCount {
weight: weight_needed,
} => {
let weight = self.total_weight - weight_needed;
Copy link
Member

Choose a reason for hiding this comment

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

This is good

self.votes.no
> votes_needed(
self.total_weight - self.votes.abstain,
Decimal::one() - percentage_needed,
Copy link
Member

Choose a reason for hiding this comment

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

Yes, this is proper place. Nice eye

)
}
Threshold::ThresholdQuorum { threshold, quorum } => {
// we always require the quorum
Copy link
Member

Choose a reason for hiding this comment

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

We don't need this check. This is just needed for is_passed

If threshold is 67% and quorum is 40%, then 35% no votes should guarantee rejected.

contracts/cw3-fixed-multisig/src/state.rs Outdated Show resolved Hide resolved
Copy link
Member

@ethanfrey ethanfrey left a comment

Choose a reason for hiding this comment

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

great work. merging

true
));

// Check edgecase where quorum is not met but we can reject
Copy link
Member

Choose a reason for hiding this comment

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

💯

@maurolacy
Copy link
Contributor

Is rejected doesn't work if the threshold deviates from 50%.

Good catch!

@ethanfrey ethanfrey merged commit c924232 into CosmWasm:main Mar 24, 2022
@ethanfrey ethanfrey mentioned this pull request Mar 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants