-
Notifications
You must be signed in to change notification settings - Fork 5
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: Protected SetRoleHolders Guard #507
Conversation
/// @inheritdoc ILlamaActionGuard | ||
/// @dev Performs a validation check at action creation time that the action creator is authorized to set the role. | ||
function validateActionCreation(ActionInfo calldata actionInfo) external view { | ||
if (BYPASS_PROTECTION_ROLE == 0 || actionInfo.creatorRole != BYPASS_PROTECTION_ROLE) { |
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.
If BYPASS_PROTECTION_ROLE
is 0 it'll always enter this if scope right? How is that bypassing it?
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.
it's clear that this is not true and when it's equal to 0 the bypass role is disabled
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.
Wait why is that? If BYPASS_PROTECTION_ROLE == 0
, your if
condition is satisfied, so you always enter the if
scope. But the idea is to bypass it right?
so shouldn't the condition be if (BYPASS_PROTECTION_ROLE != 0 && actionInfo.creatorRole != BYPASS_PROTECTION_ROLE)
?
function validateActionCreation(ActionInfo calldata actionInfo) external view { | ||
if (BYPASS_PROTECTION_ROLE == 0 || actionInfo.creatorRole != BYPASS_PROTECTION_ROLE) { | ||
RoleHolderData[] memory roleHolderData = abi.decode(actionInfo.data[4:], (RoleHolderData[])); | ||
for (uint256 i = 0; i < roleHolderData.length; i++) { |
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.
Two things to save gas (since we're on v0.8.19):
- Define length outside the loop
- Use
LlamaUtils.uncheckedIncrement(i)
for the index increment.
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.
|
||
/// @notice BYPASS_PROTECTION_ROLE can be set to 0 to disable this feature. | ||
/// This also means the all holders role cannot be set as the BYPASS_PROTECTION_ROLE. | ||
uint8 public immutable BYPASS_PROTECTION_ROLE; |
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.
What's the idea behind a Bypass Protection Role? To allow some role to bypass the action creation check? Why just 1 role in that case (and why not multiple achievable through another mapping)?
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.
I'd assume there would be cases where multiple roles will want to bypass the check.
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.
Yeah I don't really see the point of this either. If the concern setRoleHolders gets bricked then the guard can be disabled. setRoleHolder can also be used if needed. I think it would simplify things not to have it.
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.
the idea is to provide a better UX. if there is a core team role that can set every role, they shouldn't need to iteratively update the permissions mapping and update it every time a new role is created.
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.
It makes me think the whole design of this is wrong then. We should either make one of the following two assumptions:
- If this guard is enabled, all roles should default to not having any
setRoleHolders
access and any access to this function must be updated here. - If this guard is enabled, all roles should default to having normal
setRoleHolders
access and any restrictions to this function must be updated here.
This conversations makes me think we should go with 2. Having a dedicated bypass role is an extension to the Llama role system that just increases complexity.
Think about explaining this process to a smart crypto-native friend:
LlamaCore::executeAction runs the guard -> LlamaExecutor::execute -> LlamaGovernanceScript::setRoleHolders -> LlamaPolicy::setRoleHolder
It's already complicated enough.
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.
I like the idea of 2.
event AuthorizedSetRoleHolder(uint8 indexed setterRole, uint8 indexed targetRole, bool isAuthorized); | ||
|
||
/// @notice BYPASS_PROTECTION_ROLE can be set to 0 to disable this feature. | ||
/// This also means the all holders role cannot be set as the BYPASS_PROTECTION_ROLE. |
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.
Some about this screams icky to me. If the ALL HOLDERS ROLE ever held permission to call setRoleHolders on the Gov Script, it would always bypass the check? (Far fetched I know, but we should account for all possibilities.).
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.
no, that is not true at all. if you read the code, or the comment, it's clear that it would never bypass the check and setting BYPASS_PROTECTION_ROLE
to 0 (default value) disables the bypass role entirely.
/// @dev Performs a validation check at action creation time that the action creator is authorized to set the role. | ||
function validateActionCreation(ActionInfo calldata actionInfo) external view { | ||
if (BYPASS_PROTECTION_ROLE == 0 || actionInfo.creatorRole != BYPASS_PROTECTION_ROLE) { | ||
RoleHolderData[] memory roleHolderData = abi.decode(actionInfo.data[4:], (RoleHolderData[])); |
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.
Took me a second to figure out what's going on here. Might be worth it to leave a comment saying that you're slicing the bytes array to get the function calldata from the data field.
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.
Yeah agreed
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.
error UnauthorizedSetRoleHolder(uint8 setterRole, uint8 targetRole); | ||
|
||
/// @dev Emitted when the authorizedSetRoleHolder mapping is updated. | ||
event AuthorizedSetRoleHolder(uint8 indexed setterRole, uint8 indexed targetRole, bool isAuthorized); |
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.
Could call it actionCreatorRole
rather than setterRole
which seems to be a bit more accurate.
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.
/// @param setterRole The role that is is being authorized or unauthorized to set the targetRole. | ||
/// @param targetRole The role that the setterRole is being authorized or unauthorized to set. | ||
/// @param isAuthorized Whether the setterRole is authorized to set the targetRole. | ||
function setAuthorizedSetRoleHolder(uint8 setterRole, uint8 targetRole, bool isAuthorized) external { |
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.
Could call it actionCreatorRole
rather than setterRole
which seems to be a bit more accurate.
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.
address public immutable EXECUTOR; | ||
|
||
/// @notice A mapping to keep track of which roles the setterRole is authorized to set. | ||
mapping(uint8 => mapping(uint8 => bool)) public authorizedSetRoleHolder; |
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.
Can we do named mappings here?
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.
+1
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.
/// @dev Throws if called by any account other than the EXECUTOR. | ||
error OnlyLlamaExecutor(); | ||
/// @dev Throws if the setterRole is not authorized to set the targetRole. | ||
error UnauthorizedSetRoleHolder(uint8 setterRole, uint8 targetRole); |
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.
Could call it actionCreatorRole rather than setterRole which seems to be a bit more accurate.
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.
|
||
/// @title Protected Set Role Holder Guard | ||
/// @author Llama ([email protected]) | ||
/// @notice A guard that protects against unauthorized calls to setRoleHolders on the LlamaGovernanceScript. |
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.
We should be more descriptive here. Unauthorized callers can never call setRoleHolders. It's more about defining which roles can add policyholders to other roles
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.
import {ActionInfo, RoleHolderData} from "src/lib/Structs.sol"; | ||
import {ILlamaActionGuard} from "src/interfaces/ILlamaActionGuard.sol"; |
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.
The order here should be flipped.
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.
/// @dev Throws if called by any account other than the EXECUTOR. | ||
error OnlyLlamaExecutor(); | ||
/// @dev Throws if the setterRole is not authorized to set the targetRole. |
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.
Thrown not Throws
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.
And setterRole and targetRole should be in backticks
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.
/// @inheritdoc ILlamaActionGuard | ||
/// @dev Performs a validation check at action creation time that the action creator is authorized to set the role. |
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.
We shouldn't inheritdoc here right? We should just write proper natspec for these params and what the function does
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.
I don't see why not to inherit doc, or why this isn't "proper". The inherit doc already describes the params. I changed the additional comment to @notice instead of dev which defines what the function is doing.
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.
|
||
/// @title Protected Set Role Holder Guard Factory | ||
/// @author Llama ([email protected]) | ||
/// @notice A factory contract that deploys `ProtectedSetRoleHoldersGuard` contracts. |
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 put some more thought into what helpful documentation what would look like here.
Just be reading this, a Llama instance policyholder should know the purpose of this guard.
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.
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.
I think SetRoleHolderGuard
is a suitable name. It's a lot shorter and the Protected adjective doesn't do much. The purpose of every guard is to protect something.
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.
Should be SetRoleHoldersGuard
IMO. Since this is protecting setRoleHolders
and not setRoleHolder
.
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.
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.
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.
I don't know why but this link doesn't seem to take me to anything useful. can you paste what you'd like me to see here or send a screenshot
|
||
/// @dev Thrown if called by any account other than the EXECUTOR. | ||
error OnlyLlamaExecutor(); | ||
/// @dev Throw if the `actionCreatorRole` is not authorized to set the `targetRole`. |
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.
Should be an empty line here
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.
|
||
/// @dev Thrown if called by any account other than the EXECUTOR. | ||
error OnlyLlamaExecutor(); | ||
/// @dev Throw if the `actionCreatorRole` is not authorized to set the `targetRole`. |
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.
Should be Thrown not Throw
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.
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.
Small nit. But if we have a factory and a guard. We should move both files into a set-role-holders/
directory withing src/guards
. We can then just keep adding directories for other guards we might build in the future.
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.
event ProtectedSetRoleHoldersGuardDeployed( | ||
address indexed guard, uint8 indexed bypassProtectionRole, address indexed executor | ||
); |
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.
I would make this event:
event ProtectedSetRoleHoldersGuardCreated(address indexed deployer, address indexed executor, address guard, uint8 bypassProtectionRole);
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.
- For factory deploys always have the deployer/caller of the deploy as the first indexed event parameter.
- The only things you ever need to do
indexed
on is things that data consumers would want to filter by. In this case the deployer and executor. The guard is already unique so there's no need to index it. And There's no point filtering on bypassProtectionRole since it could be different roles for different instances. - We seem to be following
*Created
instead of*Deployed
for all our factory events. Let's stick to the same.
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.
import {LlamaUtils} from "src/lib/LlamaUtils.sol"; | ||
import {ActionInfo, RoleHolderData} from "src/lib/Structs.sol"; | ||
|
||
/// @title Protected Set Role Holder Guard |
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.
Should this be "Set Role Holders Guard" to reflect the contract name?
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.
Motivation:
To enable a common usecase with Llama where some roles are allowed to call
setRoleHolders
, but should only be able to set specific roles. TheProtectedSetRoleHoldersGuard
provides a check on action creation that the action creator has authorization to set the role they are trying to set.Modifications:
ProtectedSetRoleHoldersGuard
contract that inherits theILlamaActionGuard
interfacesrc/guards
ProtectedSetRoleHoldersGuard
in a new dirtests/guards/
Result:
Llama users can unlock new usecases by using the protected
setRoleHolders