Skip to content

Commit

Permalink
feat: add non delegable flag feature
Browse files Browse the repository at this point in the history
Signed-off-by: 0xRaccoon <[email protected]>
  • Loading branch information
0xRaccoon authored Dec 28, 2023
1 parent ddeb8b2 commit 39aafd7
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 24 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ broadcast/*/*/*

# Out dir
out

# Coverage
lcov.info

61 changes: 51 additions & 10 deletions solidity/contracts/governance/utils/WonderVotes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes

mapping(uint8 proposalType => Checkpoints.Trace208) private _totalCheckpoints;

mapping(address account => bool) private _nonDelegableAddresses;

/**
* @dev The clock was incorrectly modified.
*/
Expand Down Expand Up @@ -132,15 +134,21 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes
/**
* @dev Delegates votes from the sender to `delegatee`.
*/
function delegate(Delegate[] calldata delegatees, uint8 proposalType) public virtual validProposalType(proposalType) {
function delegate(
Delegate[] calldata delegatees,
uint8 proposalType
) public virtual validProposalType(proposalType) activeDelegates(delegatees) {
address account = _msgSender();
_delegate(account, proposalType, delegatees);
}

/**
* @dev See {IWonderVotes-delegate}.
*/
function delegate(address delegatee, uint8 proposalType) public virtual validProposalType(proposalType) {
function delegate(
address delegatee,
uint8 proposalType
) public virtual validProposalType(proposalType) activeDelegate(delegatee) {
address account = _msgSender();
Delegate[] memory _singleDelegate = new Delegate[](1);
_singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()});
Expand All @@ -150,7 +158,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes
/**
* @dev See {IWonderVotes-delegate}.
*/
function delegate(address delegatee) public virtual {
function delegate(address delegatee) public virtual activeDelegate(delegatee) {
address account = _msgSender();
Delegate[] memory _singleDelegate = new Delegate[](1);
_singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()});
Expand Down Expand Up @@ -187,7 +195,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes
uint8 v,
bytes32 r,
bytes32 s
) public virtual validProposalType(proposalType) {
) public virtual validProposalType(proposalType) activeDelegates(delegatees) {
if (block.timestamp > expiry) {
revert VotesExpiredSignature(expiry);
}
Expand All @@ -209,7 +217,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes
uint8 v,
bytes32 r,
bytes32 s
) public virtual validProposalType(proposalType) {
) public virtual validProposalType(proposalType) activeDelegate(delegatee) {
Delegate[] memory _singleDelegate = new Delegate[](1);
_singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()});
delegateBySig(_singleDelegate, proposalType, nonce, expiry, v, r, s);
Expand All @@ -225,7 +233,7 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes
uint8 v,
bytes32 r,
bytes32 s
) public virtual {
) public virtual activeDelegate(delegatee) {
Delegate[] memory _singleDelegate = new Delegate[](1);
_singleDelegate[0] = Delegate({account: delegatee, weight: _weightNormalizer()});

Expand All @@ -236,20 +244,35 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes
}
}

/**
* @dev See {IWonderVotes-isDelegable}.
*/
function isDelegable(address account) external view returns (bool) {
return !_nonDelegableAddresses[account];
}

/**
* @dev See {IWonderVotes-suspendDelegation}.
*/
function suspendDelegation(bool suspend) external {
_nonDelegableAddresses[msg.sender] = suspend;
emit DelegateSuspended(msg.sender, suspend);
}

/**
* @dev Delegate all of `account`'s voting units to `delegatee`.
*
* Emits events {IVotes-DelegateChanged} and {IVotes-DelegateVotesChanged}.
*/
function _delegate(address account, uint8 proposalType, Delegate[] memory delegatees) internal virtual {
if (delegatees.length > _maxDelegates()) revert DelegatesMaxNumberExceeded(delegatees.length);
if (delegatees.length > _maxDelegates()) revert VotesDelegatesMaxNumberExceeded(delegatees.length);

uint256 _weightSum;
for (uint256 i = 0; i < delegatees.length; i++) {
if (delegatees[i].weight == 0) revert ZeroWeight();
if (delegatees[i].weight == 0) revert VotesZeroWeight();
_weightSum += delegatees[i].weight;
}
if (_weightSum != _weightNormalizer()) revert InvalidWeightSum(_weightSum);
if (_weightSum != _weightNormalizer()) revert VotesInvalidWeightSum(_weightSum);

Delegate[] memory _oldDelegates = delegates(account, proposalType);

Expand Down Expand Up @@ -378,7 +401,25 @@ abstract contract WonderVotes is Context, EIP712, Nonces, IERC6372, IWonderVotes
* @dev checks the `proposalType` validity
*/
modifier validProposalType(uint8 proposalType) {
if (!_validProposalType(proposalType)) revert InvalidProposalType(proposalType);
if (!_validProposalType(proposalType)) revert VotesInvalidProposalType(proposalType);
_;
}

/**
* @dev checks if the delegation is active for the `delegatee`
*/
modifier activeDelegate(address delegatee) {
if (_nonDelegableAddresses[delegatee]) revert VotesDelegationSuspended(delegatee);
_;
}

/**
* @dev checks if the delegation is active for the `delegatees`
*/
modifier activeDelegates(Delegate[] memory delegatees) {
for (uint256 i = 0; i < delegatees.length; i++) {
if (_nonDelegableAddresses[delegatees[i].account]) revert VotesDelegationSuspended(delegatees[i].account);
}
_;
}
}
34 changes: 30 additions & 4 deletions solidity/interfaces/governance/utils/IWonderVotes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,27 @@ interface IWonderVotes {
/**
* @dev The weight delegation sum is different from weightNormalizer.
*/
error InvalidWeightSum(uint256 weightSum);
error VotesInvalidWeightSum(uint256 weightSum);

/**
* @dev The weight set for a delegate is zero.
*/
error ZeroWeight();
error VotesZeroWeight();

/**
* @dev The proposal type is invalid.
*/
error InvalidProposalType(uint8 proposalType);
error VotesInvalidProposalType(uint8 proposalType);

/**
* @dev The delegates number for a `proposalType` exceeds the maximum number of delegates.
*/
error DelegatesMaxNumberExceeded(uint256 delegateesNumber);
error VotesDelegatesMaxNumberExceeded(uint256 delegateesNumber);

/**
* @dev The delegation of votes is suspended for the account.
*/
error VotesDelegationSuspended(address account);

/**
* @dev Emitted when an account changes their delegates.
Expand All @@ -48,6 +53,12 @@ interface IWonderVotes {
*/
event DelegateVotesChanged(address indexed delegate, uint8 proposalType, uint256 previousVotes, uint256 newVotes);

/**
* @dev Emitted when the delegation of new votes is suspended or resumed for a delegate.
* Note: changing the delegation status does not affect the already delegated votes to the account.
*/
event DelegateSuspended(address indexed delegate, bool suspend);

/**
* @dev Returns the current amount of votes that `account` has for a `proposalType`.
*/
Expand Down Expand Up @@ -120,6 +131,15 @@ interface IWonderVotes {
bytes32 s
) external;

/**
* @dev The caller account can enable or disable the ability to be delegated votes by a delegator.
* If set to true, the caller account is not eligible to be a delegatee; if set to false, it can be a delegatee.
*
* NOTE: changing the delegation status does not affect the already delegated votes to the account.
* By default, all accounts are allowed to be delegated.
*/
function suspendDelegation(bool suspend) external;

/**
* @dev Returns the amount that represents 100% of the weight sum for every delegation
* used to calculate the amount of votes when partial delegating to more than 1 delegate.
Expand All @@ -136,4 +156,10 @@ interface IWonderVotes {
* @dev Returns the `proposalTypes` supported.
*/
function proposalTypes() external view returns (uint8[] memory);

/**
* @dev Returns if the account is allowed to be delegated.
* Note: changing the delegation status does not affect the already delegated votes to the account.
*/
function isDelegable(address account) external view returns (bool);
}
133 changes: 133 additions & 0 deletions solidity/test/integration/Delegation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,96 @@ contract Integration_Delegation is IntegrationBase {
}
}

function test_AllVotersDelegateAndTransferToProposerWithoutProposerDelegation() public {
for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) {
address holder = holders[_i];

Check warning on line 35 in solidity/test/integration/Delegation.t.sol

View workflow job for this annotation

GitHub Actions / Run Linters (18.x)

'holder' should start with _
vm.prank(holder);
rabbitToken.delegate(proposer);
}

for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) {
address holder = holders[_i];

for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) {
uint8 proposalType = governor.proposalTypes()[_j];
assertEq(rabbitToken.getVotes(holder, proposalType), 0);
}
}

for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) {
uint8 proposalType = governor.proposalTypes()[_j];
assertEq(rabbitToken.getVotes(proposer, proposalType), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER);
}

for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) {
address holder = holders[_i];
vm.prank(holder);
IERC20(address(rabbitToken)).transfer(proposer, INITIAL_VOTERS_BALANCE);
}

for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) {
address holder = holders[_i];

for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) {
uint8 proposalType = governor.proposalTypes()[_j];
assertEq(rabbitToken.getVotes(holder, proposalType), 0);
}
}

// Since proposer never delegated himself, and the accounts that delegates proposer has 0 tokens, proposer has now NO votes
for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) {
uint8 proposalType = governor.proposalTypes()[_j];
assertEq(rabbitToken.getVotes(proposer, proposalType), 0);
}
}

function test_AllVotersDelegateAndTransferToProposerWithProposerDelegation() public {
for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) {
address holder = holders[_i];
vm.prank(holder);
rabbitToken.delegate(proposer);
}

for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) {
address holder = holders[_i];

for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) {
uint8 proposalType = governor.proposalTypes()[_j];
assertEq(rabbitToken.getVotes(holder, proposalType), 0);
}
}

for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) {
uint8 proposalType = governor.proposalTypes()[_j];
assertEq(rabbitToken.getVotes(proposer, proposalType), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER);
}

// Proposer delegates to himself
vm.prank(proposer);
rabbitToken.delegate(proposer);

for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) {
address holder = holders[_i];
vm.prank(holder);
IERC20(address(rabbitToken)).transfer(proposer, INITIAL_VOTERS_BALANCE);
}

for (uint256 _i = 0; _i < VOTERS_NUMBER; _i++) {
address holder = holders[_i];

for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) {
uint8 proposalType = governor.proposalTypes()[_j];
assertEq(rabbitToken.getVotes(holder, proposalType), 0);
}
}

// Since proposer delegated himself, all the token transfers are now voting power of proposer
for (uint256 _j = 0; _j < governor.proposalTypes().length; _j++) {
uint8 proposalType = governor.proposalTypes()[_j];
assertEq(rabbitToken.getVotes(proposer, proposalType), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER);
}
}

function test_AllVotersDelegateByProposalType() public {
uint8[] memory proposalTypes = governor.proposalTypes();

Expand Down Expand Up @@ -149,4 +239,47 @@ contract Integration_Delegation is IntegrationBase {
assertEq(rabbitToken.getVotes(proposer2, _proposalTypes[i]), INITIAL_VOTERS_BALANCE * VOTERS_NUMBER);
}
}

function test_DelegationSuspensionDoesNotAffectPreviousVotesDelegation() public {
// Delegates himself befor suspending
vm.prank(proposer);
rabbitToken.delegate(proposer);

uint8[] memory _proposalTypes = governor.proposalTypes();

// Holder 0 delegates to proposer before suspending
vm.prank(holders[0]);
rabbitToken.delegate(proposer);

for (uint8 i = 0; i < _proposalTypes.length; i++) {
assertEq(rabbitToken.getVotes(proposer, _proposalTypes[i]), INITIAL_VOTERS_BALANCE);
}

// Suspend delegation
vm.prank(proposer);
rabbitToken.suspendDelegation(true);

// Checks that delegation suspension does not affect previous delegation
for (uint8 i = 0; i < _proposalTypes.length; i++) {
assertEq(rabbitToken.getVotes(proposer, _proposalTypes[i]), INITIAL_VOTERS_BALANCE);
}

// Previous delegation also includes transfers that the holder 0 receives
vm.prank(holders[1]);
IERC20(address(rabbitToken)).transfer(holders[0], INITIAL_VOTERS_BALANCE);

// Checks that the votes is increased
for (uint8 i = 0; i < _proposalTypes.length; i++) {
assertEq(rabbitToken.getVotes(proposer, _proposalTypes[i]), INITIAL_VOTERS_BALANCE * 2);
}

// Holder 2 delegates to proposer which previously delegated himself before suspending
vm.prank(holders[2]);
IERC20(address(rabbitToken)).transfer(proposer, INITIAL_VOTERS_BALANCE);

// Checks that the votes of proposer is increased
for (uint8 i = 0; i < _proposalTypes.length; i++) {
assertEq(rabbitToken.getVotes(proposer, _proposalTypes[i]), INITIAL_VOTERS_BALANCE * 3);
}
}
}
Loading

0 comments on commit 39aafd7

Please sign in to comment.