-
Notifications
You must be signed in to change notification settings - Fork 196
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
[VAULTS] Polish Permissions contract and other minor improvements #935
Draft
failingtwice
wants to merge
15
commits into
feat/vaults
Choose a base branch
from
permissions-polish
base: feat/vaults
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
23a4fa9
fix: remove unsafeWithdraw
failingtwice 19755bb
feat: move mass-role management to permissions
failingtwice fdb7a08
fix: rename optional fund modifier
failingtwice 0ab9aa7
feat: hooray! renaming!
failingtwice 8c43b13
feat: update role ids
failingtwice 3403a81
fix(Permissions): update role ids
failingtwice 18f184b
refactor(Permissions): hide assembly in an internal func
failingtwice 9839009
feat: log default admin in initialized event
failingtwice adeb088
feat: pass confirm lifetime as init param
failingtwice 34c2e62
feat: don't use msg.sender in init
failingtwice 038e722
fix: modifier order
failingtwice b16075c
test: permissions setup
failingtwice 3e82d3a
feat: store expiry instead of cast timestamp
failingtwice 62a8caa
fix: use raw data for mapping key
failingtwice b9dc9c3
fix: revert if roles array empty
failingtwice File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
155 changes: 155 additions & 0 deletions
155
contracts/0.8.25/utils/AccessControlMutuallyConfirmable.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
// SPDX-FileCopyrightText: 2025 Lido <info@lido.fi> | ||
// SPDX-License-Identifier: GPL-3.0 | ||
|
||
// See contracts/COMPILERS.md | ||
pragma solidity 0.8.25; | ||
|
||
import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.2/access/extensions/AccessControlEnumerable.sol"; | ||
|
||
/** | ||
* @title AccessControlMutuallyConfirmable | ||
* @author Lido | ||
* @notice An extension of AccessControlEnumerable that allows exectuing functions by mutual confirmation. | ||
* @dev This contract extends AccessControlEnumerable and adds a confirmation mechanism in the form of a modifier. | ||
*/ | ||
abstract contract AccessControlMutuallyConfirmable is AccessControlEnumerable { | ||
/** | ||
* @notice Tracks confirmations | ||
* - callId: unique identifier for the call, derived as `keccak256(msg.data)` | ||
* - role: role that confirmed the action | ||
* - timestamp: timestamp of the confirmation. | ||
*/ | ||
mapping(bytes callData => mapping(bytes32 role => uint256 expiryTimestamp)) public confirmations; | ||
|
||
/** | ||
* @notice Confirmation lifetime in seconds; after this period, the confirmation expires and no longer counts. | ||
*/ | ||
uint256 public confirmLifetime; | ||
|
||
/** | ||
* @dev Restricts execution of the function unless confirmed by all specified roles. | ||
* Confirmation, in this context, is a call to the same function with the same arguments. | ||
* | ||
* The confirmation process works as follows: | ||
* 1. When a role member calls the function: | ||
* - Their confirmation is counted immediately | ||
* - If not enough confirmations exist, their confirmation is recorded | ||
* - If they're not a member of any of the specified roles, the call reverts | ||
* | ||
* 2. Confirmation counting: | ||
* - Counts the current caller's confirmations if they're a member of any of the specified roles | ||
* - Counts existing confirmations that are not expired, i.e. lifetime is not exceeded | ||
* | ||
* 3. Execution: | ||
* - If all members of the specified roles have confirmed, executes the function | ||
* - On successful execution, clears all confirmations for this call | ||
* - If not enough confirmations, stores the current confirmations | ||
* - Thus, if the caller has all the roles, the function is executed immediately | ||
* | ||
* 4. Gas Optimization: | ||
* - Confirmations are stored in a deferred manner using a memory array | ||
* - Confirmation storage writes only occur if the function cannot be executed immediately | ||
* - This prevents unnecessary storage writes when all confirmations are present, | ||
* because the confirmations 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 _roles Array of role identifiers that must confirm the call in order to execute it | ||
* | ||
* @notice Confirmations past their lifetime are not counted and must be recast | ||
* @notice Only members of the specified roles can submit confirmations | ||
* @notice The order of confirmations does not matter | ||
* | ||
*/ | ||
modifier onlyMutuallyConfirmed(bytes32[] memory _roles) { | ||
if (_roles.length == 0) revert ZeroConfirmingRoles(); | ||
if (confirmLifetime == 0) revert ConfirmLifetimeNotSet(); | ||
|
||
uint256 numberOfRoles = _roles.length; | ||
uint256 numberOfConfirms = 0; | ||
bool[] memory deferredConfirms = new bool[](numberOfRoles); | ||
bool isRoleMember = false; | ||
|
||
for (uint256 i = 0; i < numberOfRoles; ++i) { | ||
bytes32 role = _roles[i]; | ||
|
||
if (super.hasRole(role, msg.sender)) { | ||
isRoleMember = true; | ||
numberOfConfirms++; | ||
deferredConfirms[i] = true; | ||
|
||
emit RoleMemberConfirmed(msg.sender, role, block.timestamp, msg.data); | ||
} else if (confirmations[msg.data][role] >= block.timestamp) { | ||
numberOfConfirms++; | ||
} | ||
} | ||
|
||
if (!isRoleMember) revert SenderNotMember(); | ||
|
||
if (numberOfConfirms == numberOfRoles) { | ||
for (uint256 i = 0; i < numberOfRoles; ++i) { | ||
bytes32 role = _roles[i]; | ||
delete confirmations[msg.data][role]; | ||
} | ||
_; | ||
} else { | ||
for (uint256 i = 0; i < numberOfRoles; ++i) { | ||
if (deferredConfirms[i]) { | ||
bytes32 role = _roles[i]; | ||
confirmations[msg.data][role] = block.timestamp + confirmLifetime; | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @notice Sets the confirmation lifetime. | ||
* Confirmation lifetime is a period during which the confirmation is counted. Once the period is over, | ||
* the confirmation is considered expired, no longer counts and must be recasted for the confirmation to go through. | ||
* @param _newConfirmLifetime The new confirmation lifetime in seconds. | ||
*/ | ||
function _setConfirmLifetime(uint256 _newConfirmLifetime) internal { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can track the last confirmation timestamp and revert this function if new conf precedes it |
||
if (_newConfirmLifetime == 0) revert ConfirmLifetimeCannotBeZero(); | ||
|
||
uint256 oldConfirmLifetime = confirmLifetime; | ||
confirmLifetime = _newConfirmLifetime; | ||
|
||
emit ConfirmLifetimeSet(msg.sender, oldConfirmLifetime, _newConfirmLifetime); | ||
} | ||
|
||
/** | ||
* @dev Emitted when the confirmation lifetime is set. | ||
* @param oldConfirmLifetime The old confirmation lifetime. | ||
* @param newConfirmLifetime The new confirmation lifetime. | ||
*/ | ||
event ConfirmLifetimeSet(address indexed sender, uint256 oldConfirmLifetime, uint256 newConfirmLifetime); | ||
|
||
/** | ||
* @dev Emitted when a role member confirms. | ||
* @param member The address of the confirming member. | ||
* @param role The role of the confirming member. | ||
* @param timestamp The timestamp of the confirmation. | ||
* @param data The msg.data of the confirmation (selector + arguments). | ||
*/ | ||
event RoleMemberConfirmed(address indexed member, bytes32 indexed role, uint256 timestamp, bytes data); | ||
|
||
/** | ||
* @dev Thrown when attempting to set confirmation lifetime to zero. | ||
*/ | ||
error ConfirmLifetimeCannotBeZero(); | ||
|
||
/** | ||
* @dev Thrown when attempting to confirm when the confirmation lifetime is not set. | ||
*/ | ||
error ConfirmLifetimeNotSet(); | ||
|
||
/** | ||
* @dev Thrown when a caller without a required role attempts to confirm. | ||
*/ | ||
error SenderNotMember(); | ||
|
||
/** | ||
* @dev Thrown when the roles array is empty. | ||
*/ | ||
error ZeroConfirmingRoles(); | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back 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.
let's check for the empty
_roles