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
Simple Delegation #34
base: master
Are you sure you want to change the base?
Conversation
...I would replace |
} | ||
} | ||
|
||
/** | ||
* @dev Internal function to cast a vote or object to. | ||
@dev It assumes that voter can support or object to the vote | ||
*/ | ||
function _vote(uint256 _voteId, bool _supports, address _voter) internal { | ||
function _vote(uint256 _voteId, bool _supports, address _voter, bool _isManager) internal { | ||
Vote storage vote_ = votes[_voteId]; | ||
|
||
// This could re-enter, though we can assume the governance token is not malicious | ||
uint256 voterStake = token.balanceOfAt(_voter, vote_.snapshotBlock); |
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.
LDO balance calculation can be transferred after checking that the tokenholder has already voted (to line 499)
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.
Nice catch
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.
token.balanceOfAt
is called twice and this is quite complex function inside MiniMe token, for example take a look into relatively good case second call is cost us additional 2% of gas consumption. Take a look here: https://dashboard.tenderly.co/tx/mainnet/0xc20bba17d0809089e00ffb7e69b870f5659ae0ddc296ecf7906e8419e3b56a75/gas-usage
I am suggest to consider possibility of moving _hasVotingPower
check into the _vote
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.
Addressed in 5d01f37.
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've decided to move token.balanceOfAt
call up and convert votingPower
to _vote
's argument.
…ddressList management
…check from _vote to voteFor
* test: add delegation state management unit tests * test: add delegation scenario tests (#43) * test: add delegation scenario tests * test: remove block with exception because it is pass in coverage * test: add delegation voting unit tests * test: remove outdated delegation tests --------- Co-authored-by: BATMAH69 <dev-www@yandex.ru>
@@ -24,6 +24,7 @@ contract Voting is IForwarder, AragonApp { | |||
|
|||
uint64 public constant PCT_BASE = 10 ** 18; // 0% = 0; 1% = 10^16; 100% = 10^18 | |||
|
|||
uint256 private constant UINT_96_MAX = 0xFFFFFFFFFFFFFFFFFFFFFFFF; |
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 not uint96(-1)
to not manually count the number of F
s
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're right; this way of declaring the number could seem confusing. I'll fix 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.
Fixed in 5ef1358.
/** | ||
* @notice Unassign `_delegate` from the sender | ||
*/ | ||
function resetDelegate() external { |
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.
The naming is a bit misleading here - it's easy to think that reset is like set second time - it's better to call it removeDelegate
or unassignDelegate
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.
Good catch, thank you. I'll come up with better names for these functions.
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.
Fixed in 94e3276.
@@ -73,6 +96,9 @@ contract Voting is IForwarder, AragonApp { | |||
event ChangeMinQuorum(uint64 minAcceptQuorumPct); | |||
event ChangeVoteTime(uint64 voteTime); | |||
event ChangeObjectionPhaseTime(uint64 objectionPhaseTime); | |||
event SetDelegate(address indexed voter, address indexed delegate); | |||
event ResetDelegate(address indexed voter, address indexed delegate); |
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.
the same as fro method ResetDelegate
it's also better to rename the argument delegate
to removedDelegated
or so
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.
imo "reset" is a bad verb here since it means "set again" which is not what's really meant here. a better verb would be "remove"
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.
Thanks for pointing that out; I'll come up with more appropriate names.
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.
Fixed in 94e3276.
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.
no real blockers but few suggestions/nitpicks
@@ -73,6 +96,9 @@ contract Voting is IForwarder, AragonApp { | |||
event ChangeMinQuorum(uint64 minAcceptQuorumPct); | |||
event ChangeVoteTime(uint64 voteTime); | |||
event ChangeObjectionPhaseTime(uint64 objectionPhaseTime); | |||
event SetDelegate(address indexed voter, address indexed delegate); | |||
event ResetDelegate(address indexed voter, address indexed delegate); |
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.
imo "reset" is a bad verb here since it means "set again" which is not what's really meant here. a better verb would be "remove"
* @dev Internal function to cast a vote or object to. | ||
* @dev It assumes that voter can support or object to the vote | ||
* @param _voteId The identifier of the vote | ||
* @param _supports Whether the voter supports the vote or not |
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.
the "or not" part is excessive
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.
Good catch, thanks!
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.
Fixed in c4d9e99.
|
||
uint96 voterIndex = delegates[_voter].voterIndex; | ||
assert(delegatedVoters[_delegate].addresses[voterIndex] == _voter); | ||
address lastVoter = delegatedVoters[_delegate].addresses[delegatedVotersCount - 1]; |
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.
this line can be moved inside the if
block: the code will read better and consume less gas when removing the last delegated address (incl. the only address which might be a common scenario)
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.
Agree with that; thanks for pointing it out.
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.
Fixed in 5edff75.
require(delegatedVotersCount <= UINT_96_MAX, ERROR_MAX_DELEGATED_VOTERS_REACHED); | ||
|
||
delegatedVoters[_delegate].addresses.push(_voter); | ||
delegates[_voter] = Delegate(_delegate, uint96(delegatedVotersCount)); |
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.
there's a difference in semantics between _addDelegatedAddressFor
and _removeDelegatedAddressFor
: the former updates the delegates
mapping but the latter doesn't (so it must be updated by the fn caller). this difference is not reflected in the fn names which can be misleading
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 consider moving mapping update to the _removeDelegatedAddressFor
, it will make these functions more similar, and it's not a big deal in terms of gas cost (+100).
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.
Fixed in e826ff0.
delegatedVoters[_delegate].addresses[voterIndex] = lastVoter; | ||
delegates[lastVoter].voterIndex = voterIndex; | ||
} | ||
delegatedVoters[_delegate].addresses.length--; |
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.
using .pop()
instead of .length--
would read better imo
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.
That's right, but unfortunately, Solidity v0.4.24 doesn't have .pop()
support.
function attemptVoteForMultiple(uint256 _voteId, bool _supports, address[] _voters) public voteExists(_voteId) { | ||
Vote storage vote_ = votes[_voteId]; | ||
require(_isValidPhaseToVote(vote_, _supports), ERROR_CAN_NOT_VOTE); | ||
bool hasManagedToVote = false; |
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.
votedForAtLeastOne
would be a clearer name imo
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.
Good idea.
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.
Fixed in c035a74.
* @param _supports Whether the delegate supports the vote | ||
* @param _voter address of the voter | ||
*/ | ||
function attemptVoteFor(uint256 _voteId, bool _supports, address _voter) external voteExists(_voteId) { |
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.
voteExists
calls twice - not critical, but extra unnecessary gas spendings
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.
Missed that, thanks!
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.
Fixed in 6c2840a.
require(!_supports || _getVotePhase(votes[_voteId]) == VotePhase.Main, ERROR_CAN_NOT_VOTE); | ||
_vote(_voteId, _supports, msg.sender); | ||
function vote(uint256 _voteId, bool _supports, bool /* _executesIfDecided_deprecated */) external voteExists(_voteId) { | ||
Vote storage vote_ = votes[_voteId]; |
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.
Vote storage vote_ = votes[_voteId];
duplicated line here and in the _vote
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.
After doing some tests with contracts built for cancun
hardfork, we've decided not to introduce vote_
as an argument for _vote
function. The gas cost decrease with such an approach is 72 per voter (7200 gas for 100 voters, respectively), which seems insufficient for the change that is decreasing the code readability.
* @param _voters list of voters | ||
*/ | ||
function attemptVoteForMultiple(uint256 _voteId, bool _supports, address[] _voters) public voteExists(_voteId) { | ||
Vote storage vote_ = votes[_voteId]; |
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.
Vote storage vote_ = votes[_voteId];
duplicated line here and in the _vote
It's not a lot of gas but extra 100 gas
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.
See #34 (comment)
* @param _voter address of the voter | ||
* @return True if _delegate is a current delegate for the _voter, false otherwise | ||
*/ | ||
function _isDelegateFor(address _delegate, address _voter) internal view returns (bool) { |
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 would suggest to rename _isDelegateForVoter
or _isDelegated
. Just because typical function named somethingFor
implies that first arg is something for what this function is being applyed
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.
Addressed in d33e9aa.
@@ -479,6 +750,7 @@ contract Voting is IForwarder, AragonApp { | |||
|
|||
/** | |||
* @dev Internal function to check if a vote is still open for both support and objection | |||
* @param vote_ The queried vote | |||
* @return True if less than voteTime has passed since the vote start | |||
*/ | |||
function _isVoteOpen(Vote storage vote_) internal view returns (bool) { |
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.
!vote.executed
is redundant check, since after introduction of second phase a vote couldn't be executed earlier than voteTime
has been passing
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.
@ujenjt This check might seem redundant, but it's not. There is an edge case where the vote can be open and executed at the same time. If you look at this test, you'll see that calling the unsafelyChangeVoteTime
might break the vote's state. If we remove the !vote.executed
check, with the usage of unsafelyChangeVoteTime
, it will be possible to vote for the already executed vote.
require(prevDelegate != address(0), ERROR_DELEGATE_NOT_SET); | ||
|
||
_removeDelegatedAddressFor(prevDelegate, msg.sender); | ||
delete delegates[msg.sender]; |
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.
clearing of delegates mapping happening after the event ResetDelegate
has been emited which is a bit confusing
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.
Thanks for pointing this out. As mentioned earlier, I'll do some refactoring to ensure that ResetDelegate
will be emitted after the deletion.
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.
Fixed in e826ff0.
* @param _voters the list of voters | ||
* @param _voteId Vote identifier | ||
*/ | ||
function getVotersStateAtVote(uint256 _voteId, address[] _voters) external view voteExists(_voteId) returns (VoterState[] memory voterStatesList) { |
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.
This function is batch accessor for the getVoterState
- it's better to name it getVoterStateMultiple
for example
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.
Addressed in cd8697b.
@@ -73,6 +96,9 @@ contract Voting is IForwarder, AragonApp { | |||
event ChangeMinQuorum(uint64 minAcceptQuorumPct); | |||
event ChangeVoteTime(uint64 voteTime); | |||
event ChangeObjectionPhaseTime(uint64 objectionPhaseTime); | |||
event SetDelegate(address indexed voter, address indexed delegate); | |||
event ResetDelegate(address indexed voter, address indexed delegate); | |||
event CastVoteAsDelegate(uint256 indexed voteId, address indexed delegate, address indexed voter, bool supports, uint256 stake); |
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.
Maybe let's optimize CastVoteAsDelegate
event and cut off supports
and stake
fields? it's additional 2*256 units of gas! Or maybe get rid of indexed params?
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.
See this comment — #34 (comment)
} else { | ||
vote_.nay = vote_.nay.add(voterStake); | ||
vote_.voters[_voter] = VoterState.Nay; | ||
vote_.voters[_voter] = _isDelegate ? VoterState.DelegateNay : VoterState.Nay; | ||
} | ||
|
||
emit CastVote(_voteId, _voter, _supports, voterStake); | ||
|
||
if (_getVotePhase(vote_) == VotePhase.Objection) { | ||
emit CastObjection(_voteId, _voter, voterStake); |
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.
Do we still need a separate event for it? Maybe let's consider to add phase
parameter to CastVote event? It depends on frontend / subgraph / tooling tho
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've decided to keep these two events as they were since changing them may result in bad backward compatibility.
…ve duplicated tests
*/ | ||
function _removeDelegatedAddressFor(address _delegate, address _voter) internal { | ||
uint256 delegatedVotersCount = delegatedVoters[_delegate].addresses.length; | ||
require(delegatedVotersCount > 0, ERROR_DELEGATE_NOT_SET); |
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.
Excess check - never triggers
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.
next assert actually checking more strict condition
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.
Good one, thank you!
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.
Fixed in 8ec459c.
* @param _voter address of the voter | ||
*/ | ||
function _removeDelegatedAddressFor(address _delegate, address _voter) internal { | ||
uint256 delegatedVotersCount = delegatedVoters[_delegate].addresses.length; |
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.
better to change to lastVoterIndex = delegatedVoters[_delegate].addresses.length - 1
and simplify the code later
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.
delegatedVoters[_delegate]
might be extracted to the separate variable to save some gas
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.
Nice findings, thanks 🙏
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.
Fixed in 8ec459c.
delegates[lastVoter].voterIndex = voterIndex; | ||
} | ||
delegatedVoters[_delegate].addresses.length--; | ||
emit ResetDelegate(_voter, _delegate); |
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.
technically at this line delegate is not completely reset, since it still in the delegates mapping, I suggest to move events out of this func
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.
Maybe make ordering of voting and delegate the same?
function _removeDelegatedAddressFor(address _delegate, address _voter)
emit ResetDelegate(_voter, _delegate)
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.
technically at this line delegate is not completely reset, since it still in the delegates mapping, I suggest to move events out of this func
Fixed in 8ec459c.
The Simple Delegation update allows LDO token holders to delegate their voting power to other addresses and delegates to participate in on-chain voting on behalf of their delegated voters.
Spec