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

feat: add smart and partial delegation #3

Merged
merged 5 commits into from
Dec 20, 2023

Conversation

0xRaccoon
Copy link
Member


mapping(address account => address) private _delegatee;
mapping(address account => mapping(uint8 proposalType => Delegate[])) private _delegatees;
Copy link
Collaborator

Choose a reason for hiding this comment

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

have you considered to use a single mapping with the hash(account, proposalType)? I guess it would be cheaper

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I considered it. We could discuss it. The main tradeoff that I see with the hash approach is that we would have to perform a hashing operation when the user votes and when delegates (in this case 1, hashing operation per account-proposalType) combination.

Copy link
Member Author

Choose a reason for hiding this comment

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

@skeletor-spaceman any thoughts about this one?

Copy link
Collaborator

Choose a reason for hiding this comment

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

hashing is done either way on the mapping, so it does not really optimizes anything, nested mappings are easier and cleaner to read.


mapping(address delegatee => Checkpoints.Trace208) private _delegateCheckpoints;
mapping(address delegatee => mapping(uint8 proposalType => Checkpoints.Trace208)) private _delegateCheckpoints;
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here

if (delegatees[i].weight == 0) revert ZeroWeight();
_weightSum += delegatees[i].weight;
}
if (_weightSum != _totalWeight()) revert InvalidWeightSum(_weightSum);
Copy link
Collaborator

Choose a reason for hiding this comment

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

an alternative approach (but don't think its worth) is normalizing, i.e. considering the proportion and translating to the real weights

Copy link
Collaborator

Choose a reason for hiding this comment

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

that would even allow users to input percentages for example instead of precise uints

Copy link
Member Author

@0xRaccoon 0xRaccoon Dec 18, 2023

Choose a reason for hiding this comment

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

mmm not sure if I get this one, could you provide an example?. If I translate to real weight wouldn't that one be a float?.

In this approach, the protocol implements the totalWeight() which will represent the 100% of weight that is used a control that when delegating the weightSum of the delegatees is always equal to totalWeight.

Eg. Let's suppose that a user delegates his votes (let's abstract the token decimals since it's transparent for us, and also the proposalTypes since we know that it will apply to all of them):

totalWeight = 255; (defined by the protocol and applies to all delegations)
tokens held by user = 100;

The user delegates to 3 different delegates, knowing that he has to distribute a total 255 in weights units

delegate1_weight = 95;
delegate2_weight = 50;
delegate3_weight = 110;

The formula is:
voting_power = delegator_tokens_held * weight / totalWeight;

The vote power distribution will be:

delegate1_votes = 100 * 95 / 255 => ≈  37.25;
delegate2_votes = 100 * 50 / 255 => ≈  19.60;
delegate3_votes = 100 * 110 / 255 => ≈  43.14;

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 block of code only controls that the weightSum is 100%, _moveDelegateVotes is the one that calculates the votes according to each weight

Copy link
Collaborator

Choose a reason for hiding this comment

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

Really appreciate the details! What I meant was maybe not reverting if the sum of the weights doesnt add up to totalWeight(), and just finding out the proportion.

Example: suppose totalWeight=255, then a user can define

delegate1_weight = 20; //20%
delegate2_weight = 50; // 50%
delegate3_weight = 30; //30%

and then you can compute uint256 sumedWeights = delegate1_weight + delegate2_weight + delegate3_weight, and consider the proportion as (delegate1_weight * totalWeight/sumedWeights).
Weights can be anything here, doesnt even need to add up to 100. Just compute the proportion.

I'm not sure if this is better, I think its a little bit more user friendly, but I might be mistaken

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 considered this approach using relative weights; it's more flexible. The problem here might be that, in some conditions, it could overflow; let's suppose that a user delegates to two delegates with a considerable weight number but with a low balance. Then, if the user's balance significantly increments, for example, it could overflow when transferring the tokens and give the protocol less control over that.

Copy link
Collaborator

Choose a reason for hiding this comment

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

ok, so end of the day, using totalWeight is a way of ensuring amounts are not excesive. Another way to avoid this while keeping relative weights is by using a maxAmount, like forcing the sum up to 100 (see example above). Again, I'm not sure which one is better UX wise, but I lean to think percentages over amounts is more clear.

Copy link
Member Author

Choose a reason for hiding this comment

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

yes we could do this control, we would still have the downside that when transferring tokens for example we will always have to sum up all the weights of the arrays instead of using a straight calculation which will introduce more overhead

@0xParticle
Copy link
Collaborator

0xParticle commented Dec 18, 2023

I'm a bit confused by the use of totalWeight. Where is it defined? Does it use percentages that sum up to 1 (or 100) or is it an amount for each delegatee?
The rest looks clean to me

@0xRaccoon
Copy link
Member Author

0xRaccoon commented Dec 18, 2023

I'm a bit confused by the use of totalWeight. Where is it defined? Does it use percentages that sum up to 1 (or 100) or is it an amount for each delegatee? The rest looks clean to me

Since Votes is an abstract contract then we can define some methods that will need to be overridden by the implementation token and that can be up to each protocol. One of them is totalWeight() this one is used to calculate the voting power distribution between delegates and will represent how granular is the weight precision, In the current implementation we used a uint8 but we could even define a more precise type like uint256 if we think that it will be worth, It will be responsibility of the protocols to check for the overflows if they decide to use a big totalWeight.
If the user defines a totalWeight = 255 this means that 100% weight = 255 and will be always used as a divisor to calculate the voting power

address oldDelegate = delegates(account);
_delegatee[account] = delegatee;
function _delegate(address account, uint8 proposalType, Delegate[] memory delegatees) internal virtual {
if (delegatees.length > _maxDelegates()) revert DelegatesMaxNumberExceeded(delegatees.length);
Copy link
Collaborator

Choose a reason for hiding this comment

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

what is _maxDelegates used for? total maxDelegates of a user? or per operation?

Copy link
Member Author

@0xRaccoon 0xRaccoon Dec 19, 2023

Choose a reason for hiding this comment

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

the max number of delegates that a user can delegate for a proposalType. This applies to all proposals and it's defined by the protocol

Copy link
Collaborator

Choose a reason for hiding this comment

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

got it, so on _delegate you need to always supply the entire list. you cannot add or modify only 1 (or a subset) delegatees. right? that makes sense

Copy link
Member Author

Choose a reason for hiding this comment

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

exactly

if (delegatees[i].weight == 0) revert ZeroWeight();
_weightSum += delegatees[i].weight;
}
if (_weightSum != _totalWeight()) revert InvalidWeightSum(_weightSum);
Copy link
Collaborator

Choose a reason for hiding this comment

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

why does it need to be equal? can it not just be lower or equal than total? i.e. you don't want to assign all of your weight.

also, what happens when you transfer tokens out?
who out of these delegatees will have a reduced weight? all of them? that's a bit expensive gas-wise
and on tokens in? who gets the increase in weight? all of them or none?
maybe we should consider an ordered list of delegatees, where votes are taken from the bottom-up and assigned to the top one. since looping on proposalTypes + delegatees is going to be a gas nightmare

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 is done for optimization every time you decide to distribute your voting power for proposal you will have to define the entire distribution. If we don't delegate the 100% of the weight, then we will have to allow the user to add more delegations to the same proposals, controlling the percentages that we already assigned which will introduce overhead. Another way if you mean this would be if delegating a percentage of weight that is not the 100% means that the user always keeps the rest of the voting power. And a new delegation will override the previous one for a proposalType that would make sense. But kinda could be the same that the current approach if the user delegates himself in the array with the weight desired to keep.

when transferring tokens _transferVotingUnits is called, adding or substracting voting power to the corresponding addresses.
All the delegates with the from account for all the proposalTypes votes will be reduced and all the delegates with the to will be incremented. (_transferVotingUnits and _moveDelegateVotes) functions.

Could you expand on the ordered list of delegates solution?. Since all the delegates from the sender and all the delegates of the destination for all proposalTypes will be affected I'm not sure how this will help with the gas efficiency

Copy link
Collaborator

@skeletor-spaceman skeletor-spaceman Dec 19, 2023

Choose a reason for hiding this comment

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

Could you expand on the ordered list of delegates solution?. Since all the delegates from the sender and all the delegates of the destination for all proposalTypes will be affected I'm not sure how this will help with the gas efficiency

yes, i'm trying to figure out a way to avoid having to loop over all proposalTypes * all delegatees. but it might not be a desirable UX.

0xParticle
0xParticle previously approved these changes Dec 19, 2023
…or-poc into feat/add-smart-and-partial-delegation

Signed-off-by: 0xRaccoon <[email protected]>
@0xRaccoon 0xRaccoon requested a review from gas1cent December 20, 2023 13:46
@0xRaccoon 0xRaccoon merged commit c4a138a into dev Dec 20, 2023
6 checks passed
@0xRaccoon 0xRaccoon deleted the feat/add-smart-and-partial-delegation branch December 20, 2023 14:24
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.

4 participants