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

Simple Delegation #34

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

Simple Delegation #34

wants to merge 52 commits into from

Conversation

ujenjt
Copy link
Member

@ujenjt ujenjt commented Jan 25, 2024

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

apps/voting/contracts/Voting.sol Outdated Show resolved Hide resolved
apps/voting/contracts/Voting.sol Outdated Show resolved Hide resolved
@krogla
Copy link

krogla commented Jan 25, 2024

...I would replace voteFor with a method like voteByManager, where I wouldn't pass a list of addresses, but simply check if the msg.sender address has any under its management, and if so, vote on their behalf in a loop. Of course, it would require adding chunks, but then it seems we could simplify the logic of checking whether the manager's vote matches the delegate's vote.

}
}

/**
* @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);
Copy link

@zuzueeka zuzueeka Jan 30, 2024

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)

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice catch

Copy link
Member Author

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

Choose a reason for hiding this comment

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

Addressed in 5d01f37.

Copy link

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.

bulbozaur
bulbozaur previously approved these changes Mar 21, 2024
katamarinaki and others added 6 commits March 27, 2024 23:30
* 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;
Copy link
Member Author

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 Fs

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.

Copy link

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 {
Copy link
Member Author

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

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.

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);
Copy link
Member Author

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

Copy link
Member

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"

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.

Choose a reason for hiding this comment

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

Fixed in 94e3276.

Copy link
Member

@skozin skozin left a 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);
Copy link
Member

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"

apps/voting/contracts/Voting.sol Show resolved Hide resolved
* @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
Copy link
Member

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

Choose a reason for hiding this comment

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

Good catch, thanks!

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];
Copy link
Member

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)

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.

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));
Copy link
Member

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

Copy link

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).

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--;
Copy link
Member

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

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;
Copy link
Member

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

Choose a reason for hiding this comment

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

Good idea.

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) {
Copy link
Member Author

@ujenjt ujenjt Apr 14, 2024

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

Choose a reason for hiding this comment

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

Missed that, thanks!

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];
Copy link
Member Author

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

Copy link

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];
Copy link
Member Author

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

Choose a reason for hiding this comment

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

* @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) {
Copy link
Member Author

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

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) {
Copy link
Member Author

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

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];
Copy link
Member Author

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

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.

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) {
Copy link
Member Author

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

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);
Copy link
Member Author

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?

Copy link

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);
Copy link
Member Author

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

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.

*/
function _removeDelegatedAddressFor(address _delegate, address _voter) internal {
uint256 delegatedVotersCount = delegatedVoters[_delegate].addresses.length;
require(delegatedVotersCount > 0, ERROR_DELEGATE_NOT_SET);
Copy link
Member Author

Choose a reason for hiding this comment

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

Excess check - never triggers

Copy link
Member Author

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

Choose a reason for hiding this comment

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

Good one, thank you!

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;
Copy link
Member Author

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

Copy link
Member Author

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

Choose a reason for hiding this comment

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

Nice findings, thanks 🙏

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);
Copy link
Member Author

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

Copy link
Member Author

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)

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.

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