Skip to content
This repository has been archived by the owner on Nov 27, 2024. It is now read-only.

feat: Protected SetRoleHolders Guard #507

Closed
wants to merge 21 commits into from
Closed

Conversation

dd0sxx
Copy link
Contributor

@dd0sxx dd0sxx commented Jan 5, 2024

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. The ProtectedSetRoleHoldersGuard provides a check on action creation that the action creator has authorization to set the role they are trying to set.

Modifications:

  • created a ProtectedSetRoleHoldersGuard contract that inherits the ILlamaActionGuard interface
  • Added a new dir src/guards
  • Wrote a test suite for ProtectedSetRoleHoldersGuard in a new dir tests/guards/

Result:

Llama users can unlock new usecases by using the protected setRoleHolders

Copy link

github-actions bot commented Jan 5, 2024

Coverage after merging theo/protectedSetRoleHolder into main will be

88.62%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   LlamaCore.sol99.65%98.72%100%100%299
   LlamaExecutor.sol80%50%100%100%33
   LlamaFactory.sol100%100%100%100%
   LlamaLens.sol72.09%20%100%82.61%174–175, 178, 178, 178–179, 179, 179–180, 180, 180, 188
   LlamaPolicy.sol91.53%88.89%94.59%91.38%353, 392, 392, 392–394, 394, 394, 396–399, 401, 419
   LlamaPolicyMetadata.sol100%100%100%100%
src/accounts
   LlamaAccount.sol100%100%100%100%
   LlamaAccountWithDelegation.sol100%100%100%100%
src/guards
   ProtectedSetRoleHoldersGuard.sol83.33%50%100%100%36, 39, 51
src/lib
   ERC721NonTransferableMinimalProxy.sol70.42%72.73%72.73%68.42%102, 104, 106, 118–120, 190, 88, 88, 88, 90, 90, 90, 92, 92, 92, 97, 99
   LlamaUtils.sol100%100%100%100%
   PolicyholderCheckpoints.sol55.88%50%81.82%53.62%130, 184–186, 186, 186–187, 189, 192, 222, 229, 229–231, 233, 233–235, 237, 237–239, 241, 241–243, 262, 265–271, 278, 46, 46–48, 48, 48–49, 51
   SupplyCheckpoints.sol57.14%50%83.33%54.93%131, 183–185, 185, 185–186, 188, 191, 235, 242, 242–244, 246, 246–248, 250, 250–252, 254, 254–256, 275, 278–284, 291, 50, 50–52, 52, 52–53, 55
src/llama-scripts
   LlamaAccountTokenDelegationScript.sol100%100%100%100%
   LlamaGovernanceScript.sol96.24%64.29%100%100%131, 133, 160, 171, 228
   LlamaInstanceConfigBase.sol100%100%100%100%
   LlamaInstanceConfigScriptTemplate.sol100%100%100%100%
src/strategies/absolute
   LlamaAbsolutePeerReview.sol100%100%100%100%
   LlamaAbsoluteQuorum.sol100%100%100%100%
   LlamaAbsoluteStrategyBase.sol94.81%87.50%90.91%98%248, 251, 301
src/strategies/relative
   LlamaRelativeHolderQuorum.sol91.67%75%100%100%52, 59
   LlamaRelativeQuantityQuorum.sol0%0%0%0%28, 28, 28–30, 42, 42, 42–44, 51–52, 52, 52–53, 58–59, 59, 59–60
   LlamaRelativeStrategyBase.sol97.62%90.91%100%100%208, 310
   LlamaRelativeUniqueHolderQuorum.sol93.33%83.33%100%100%54, 61

/// @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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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++) {
Copy link
Contributor

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):

  1. Define length outside the loop
  2. Use LlamaUtils.uncheckedIncrement(i) for the index increment.

Copy link
Contributor Author

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;
Copy link
Contributor

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)?

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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:

  1. 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.
  2. 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.

Copy link
Contributor

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.
Copy link
Contributor

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.).

Copy link
Contributor Author

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[]));
Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah agreed

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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 (devsdosomething@llama.xyz)
/// @notice A guard that protects against unauthorized calls to setRoleHolders on the LlamaGovernanceScript.
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 4 to 5
import {ActionInfo, RoleHolderData} from "src/lib/Structs.sol";
import {ILlamaActionGuard} from "src/interfaces/ILlamaActionGuard.sol";
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 11 to 13
/// @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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Thrown not Throws

Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 33 to 34
/// @inheritdoc ILlamaActionGuard
/// @dev Performs a validation check at action creation time that the action creator is authorized to set the role.
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 (devsdosomething@llama.xyz)
/// @notice A factory contract that deploys `ProtectedSetRoleHoldersGuard` contracts.
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

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`.
Copy link
Contributor

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

Copy link
Contributor Author

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`.
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 11 to 13
event ProtectedSetRoleHoldersGuardDeployed(
address indexed guard, uint8 indexed bypassProtectionRole, address indexed executor
);
Copy link
Contributor

@0xrajath 0xrajath Jan 8, 2024

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);

Copy link
Contributor

@0xrajath 0xrajath Jan 8, 2024

Choose a reason for hiding this comment

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

  1. For factory deploys always have the deployer/caller of the deploy as the first indexed event parameter.
  2. 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.
  3. We seem to be following *Created instead of *Deployed for all our factory events. Let's stick to the same.

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants