-
Notifications
You must be signed in to change notification settings - Fork 330
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
cc96e9f
commit b4e4236
Showing
4 changed files
with
502 additions
and
0 deletions.
There are no files selected for viewing
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,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.
Sorry, something went wrong. |
||
return accounts[account].delegatedContractSelectors[delegate][target].values(); | ||
} | ||
} |
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,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; | ||
} |
Oops, something went wrong.
chore: forge fmt src/contracts