Skip to content

Commit

Permalink
Merge pull request #888 from lidofinance/committee-replace-bitmap
Browse files Browse the repository at this point in the history
fix: use array instead of bitmap
  • Loading branch information
mymphe authored Dec 2, 2024
2 parents 6d8426f + 417d433 commit d534778
Showing 1 changed file with 43 additions and 8 deletions.
51 changes: 43 additions & 8 deletions contracts/0.8.25/vaults/Delegation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -392,32 +392,67 @@ contract Delegation is Dashboard, IReportReceiver {
}

/**
* @dev Modifier that requires approval from all committee members within a voting period.
* Uses a bitmap to track new votes within the call instead of updating storage immediately.
* @param _committee Array of role identifiers that form the voting committee.
* @param _votingPeriod Time window in seconds during which votes remain valid.
* @dev Modifier that implements a mechanism for multi-role committee approval.
* Each unique function call (identified by msg.data: selector + arguments) requires
* approval from all committee role members within a specified time window.
*
* The voting process works as follows:
* 1. When a committee member calls the function:
* - Their vote is counted immediately
* - If not enough votes exist, their vote is recorded
* - If they're not a committee member, the call reverts
*
* 2. Vote counting:
* - Counts the current caller's votes if they're a committee member
* - Counts existing votes that are within the voting period
* - All votes must occur within the same voting period window
*
* 3. Execution:
* - If all committee members have voted within the period, executes the function
* - On successful execution, clears all voting state for this call
* - If not enough votes, stores the current votes
* - Thus, if the caller has all the roles, the function is executed immediately
*
* 4. Gas Optimization:
* - Votes are stored in a deferred manner using a memory array
* - Vote storage writes only occur if the function cannot be executed immediately
* - This prevents unnecessary storage writes when all votes are present,
* because the votes are cleared anyway after the function is executed,
* - i.e. this optimization is beneficial for the deciding caller and
* saves 1 storage write for each role the deciding caller has
*
* @param _committee Array of role identifiers that form the voting committee
* @param _votingPeriod Time window in seconds during which votes remain valid
*
* @notice Votes expire after the voting period and must be recast
* @notice All committee members must vote within the same voting period
* @notice Only committee members can initiate votes
*
* @custom:security-note Each unique function call (including parameters) requires its own set of votes
*/
modifier onlyIfVotedBy(bytes32[] memory _committee, uint256 _votingPeriod) {
bytes32 callId = keccak256(msg.data);
uint256 committeeSize = _committee.length;
uint256 votingStart = block.timestamp - _votingPeriod;
uint256 voteTally = 0;
uint256 votesToUpdateBitmap = 0;
bool[] memory deferredVotes = new bool[](committeeSize);
bool isCommitteeMember = false;

for (uint256 i = 0; i < committeeSize; ++i) {
bytes32 role = _committee[i];

if (super.hasRole(role, msg.sender)) {
isCommitteeMember = true;
voteTally++;
votesToUpdateBitmap |= (1 << i);
deferredVotes[i] = true;

emit RoleMemberVoted(msg.sender, role, block.timestamp, msg.data);
} else if (votings[callId][role] >= votingStart) {
voteTally++;
}
}

if (votesToUpdateBitmap == 0) revert NotACommitteeMember();
if (!isCommitteeMember) revert NotACommitteeMember();

if (voteTally == committeeSize) {
for (uint256 i = 0; i < committeeSize; ++i) {
Expand All @@ -427,7 +462,7 @@ contract Delegation is Dashboard, IReportReceiver {
_;
} else {
for (uint256 i = 0; i < committeeSize; ++i) {
if ((votesToUpdateBitmap & (1 << i)) != 0) {
if (deferredVotes[i]) {
bytes32 role = _committee[i];
votings[callId][role] = block.timestamp;
}
Expand Down

0 comments on commit d534778

Please sign in to comment.