Skip to content

Commit

Permalink
Added:
Browse files Browse the repository at this point in the history
1) Interface for composable permission controller.
2) Storage layout for permission controller.
3) Initial implementation for controller.

I also added a library for a bytes4 enumerable set, since OZ didn't have
one. We do this so people can easily inspect which permissions on which
contracts people have. We could also add an index for which contracts
have permissions, but I figured that getting the list of important
addresses (core contracts + AVS contracts) would be far more legibile
off-chain. We could maintain it on chain and provide that list of
contracts, and quite frankly it would be great because then you could
determine ALL permissions that a user had simply with view functions. I
might add this incrementally if we can align on the basics here.
  • Loading branch information
scotthconner committed Oct 11, 2024
1 parent cc96e9f commit b4e4236
Show file tree
Hide file tree
Showing 4 changed files with 502 additions and 0 deletions.
115 changes: 115 additions & 0 deletions src/contracts/core/PermissionController.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;

import "./PermissionControllerStorage.sol";

contract PermissionController is PermissionControllerStorage {
// enable library functions on enumerable set
using EnumerableSet for EnumerableSet.AddressSet;
using EnumerableBytes4Set for EnumerableBytes4Set.Bytes4Set;

/**
* onlyValidAccounts
*
* This modifier is used to ensure that account access only
* occurs on valid accounts.
*/
modifier onlyValidAccounts(address account) {
require(accounts[account].isValid, InvalidAccount());
_;
}

/**
* onlyAccountAdmin
*
* This modifier is used to ensure that functions can only be accessed
* by message senders that are admins for the given account.
*
*/
modifier onlyAccountAdmin(address account) {
require(accounts[account].admins.contains(msg.sender), CallerNotAdmin());
_;
}

//////////////////////////////////////////////////
// Public Management Interface
//////////////////////////////////////////////////

/// @inheritdoc IPermissionController
function createAccount() external {
// we assume the message sender is an AVS, Operator, or Staker, but we don't
// necessarily verify this. There is no harm in any address creating its own
// delegated account. It's not meaningfully possible to "grief" account creation.
DelegatedAccountInfo storage account = accounts[msg.sender];

// check to make sure the account doesn't already exist
require(!account.isValid, AccountAlreadyExists());

// make the account valid by marking it as such, and by default add the
// message sender as an admin. The account address is not a permanent
// admin, so its inclusion in the set is critical.
account.isValid = true;
account.admins.add(msg.sender);

emit DelegatedAccountCreated(msg.sender);
}

/// @inheritdoc IPermissionController
function setAccountAdmin(address account, address delegate, bool isAdmin) onlyValidAccounts(account) onlyAccountAdmin(account) external {
// at this point we know the caller is admin on a valid account, so
// we can simply set the state and emit an event
if (isAdmin) {
accounts[account].admins.add(delegate);
} else {
accounts[account].admins.remove(delegate);
}

emit DelegatedAccountAdminChange(account, msg.sender, delegate, isAdmin);
}

/// @inheritdoc IPermissionController
function setDelegatedRole(
address account,
address target,
bytes4 selector,
address delegate,
bool hasPermission) onlyValidAccounts(account) onlyAccountAdmin(account) external {

// at this point we know the caller is admin on a valid account,
// so we can simply set the state, update the index and emit an event
DelegatedAccountInfo storage info = accounts[account];
info.delegations[delegate][target][selector] = hasPermission;
if (hasPermission) {
info.delegatedContractSelectors[delegate][target].add(selector);
} else {
info.delegatedContractSelectors[delegate][target].remove(selector);
}
}

//////////////////////////////////////////////////
// Public Introspection Interface
//////////////////////////////////////////////////

/// @inheritdoc IPermissionController
function isValidAccount(address account) external view returns (bool) {
return accounts[account].isValid;
}

/// @inheritdoc IPermissionController
function hasDelegationOrAdmin(address account, address target, bytes4 selector, address delegate) external view returns (bool) {
return accounts[account].delegations[delegate][target][selector] || accounts[account].admins.contains(delegate);
}

/// @inheritdoc IPermissionController
function getAccountAdmins(address account) onlyValidAccounts(account) external view returns (address[] memory) {
return accounts[account].admins.values();
}

/// @inheritdoc IPermissionController
function getAccountPermissions(
address account,
address delegate,
address target) onlyValidAccounts(account) external view returns (bytes4[] memory) {

This comment has been minimized.

Copy link
@0xClandestine

0xClandestine Oct 16, 2024

Contributor

chore: forge fmt src/contracts

return accounts[account].delegatedContractSelectors[delegate][target].values();
}
}
46 changes: 46 additions & 0 deletions src/contracts/core/PermissionControllerStorage.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.27;
import "../interfaces/IPermissionController.sol";

// We use Enumerable set to maintain an index. While we do
// spend a little more gas, the amount of money spent on gas
// maintaining this index is far less than the operations required
// to maintain off-chain indexes and serve this information over RPC
// to users. Storing a little bit more on-chain passes the "walk away test"
// which, for protocol account permissions, seems critical.
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

// we created a quick EnumerableSet for Bytes4 since OZ didn't have one
import "../libraries/EnumerableBytes4Set.sol";

abstract contract PermissionControllerStorage is IPermissionController {
/**
* DelegatedAccountInfo
*
* This stucture stores all of the information contained specifically
* for a single delegated account. The account ID is the address used
* in the key for the mapping this structure is in, and will not be
* duplicated as part of the structure.
*/
struct DelegatedAccountInfo {
// We store a sanity flag here to ensure that we can identify
// an invalid account from one with no admins (fully revoked)
bool isValid; // EVM defaults this to false

// There can be multiple admins for a given account, so
// we will add and remove from this set as a way to determine
// if they are an admin or not
EnumerableSet.AddressSet admins;

// delegate => contract => selector => state
mapping(address => mapping(address => mapping(bytes4 => bool))) delegations;

// we will also be maintaining a "walk away" index so introspecting
// on existing permissions does not require off-chain indexing.
// delegate => contract => selectors
mapping(address => mapping(address => EnumerableBytes4Set.Bytes4Set)) delegatedContractSelectors;
}

// The mapping of account address to all of the delegated account info for it.
mapping(address => DelegatedAccountInfo) internal accounts;
}
Loading

0 comments on commit b4e4236

Please sign in to comment.