Skip to content

Commit

Permalink
docs: add PermissionController doc
Browse files Browse the repository at this point in the history
  • Loading branch information
nadir-akhtar committed Dec 4, 2024
1 parent ad265de commit 766fe4a
Showing 1 changed file with 190 additions and 0 deletions.
190 changes: 190 additions & 0 deletions docs/release/slashing/PermissionController.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# PermissionController

| File | Type | Proxy |
| -------- | -------- | -------- |
| [`PermissionController.sol`](../../src/contracts/permissions/PermissionController.sol) | Singleton | Transparent proxy |

The `PermissionController` handles user permissions. An "account" refers to an address.

The `PermissionController` defines three different roles:
* [Accounts](#accounts)
* [Admins](#admins)
* [Appointees](#appointees)

The core contracts using the `PermissionController` as a dependency are the:
* `DelegationManager`
* `AllocationManager`
* `RewardsCoordinator`

Note that the `AVSDirectory` will soon be deprecated and thus does not support this contract.

---

## Accounts

Accounts refer to the Ethereum address through which one interacts with the protocol. Accounts have the ability to set and remove admins and/or appointees, which can take actions on an account's behalf depending on their role.

Note that an account cannot appoint an address to *register* on its behalf. In other words, the address a user registers with is their account, and they can then transfer admin of that account in a subsequent tx. Similarly, the account of an AVS is the address they use to initialize state in the `AllocationManager`.

### Account Permissions

Account permissions are stored within a struct defined as follows:

```solidity
struct AccountPermissions {
/// @notice The pending admins of the account
EnumerableSet.AddressSet pendingAdmins;
/// @notice The admins of the account
EnumerableSet.AddressSet admins;
/// @notice Mapping from an appointee to the list of encoded target & selectors
mapping(address appointee => EnumerableSet.Bytes32Set) appointeePermissions;
/// @notice Mapping from encoded target & selector to the list of appointees
mapping(bytes32 targetSelector => EnumerableSet.AddressSet) permissionAppointees;
}
```

These structs are then stored within a mapping defined as follows, allowing for fetching account permissions for a given account with ease:

```solidity
mapping(address account => AccountPermissions) internal _permissions;
```

By default, no other address can perform an action on behalf of a given account. However, accounts can add admins and/opr appointees to give other addresses the ability to act on their behalf.

## Admins

Admins are able to take any action on behalf of an original account -- including adding or removing admins. This enables key rotation for operators, or creating a backup admin which is stored on a cold key.

### Adding an Admin

The relevant functions for admin addition are:

* [`addPendingAdmin`](#addpendingadmin)
* [`removePendingAdmin`](#removependingadmin)
* [`acceptAdmin`](#acceptadmin)

#### `addPendingAdmin`

```solidity
function addPendingAdmin(address account, address admin) external onlyAdmin(account);
```

When adding an admin, an account must call `addPendingAdmin()`. Then, the pending admin must call `acceptAdmin()` for the given account, completing the process. An account cannot force an admin role upon another account.

*Effects*:
* An address is added to the `pendingAdmins` set for the account
* A `PendingAdminAdded` event is emitted specifying the account for which a pending admin was added

*Requirements*:
* The proposed admin MUST NOT already be an admin for the account
* The proposed admin MUST NOT be a pending admin for the account
* Caller MUST be an admin for the account, or the account's address itself if no admin is set

#### `removePendingAdmin`

```solidity
function removePendingAdmin(address account, address admin) external onlyAdmin(account);
```

An account, if it wishes to retract the pending admin offer, can call `removePendingAdmin()` to prevent the potential admin from accepting their role. However, this will only work if `acceptAdmin()` has not yet been called by the potential admin.

*Effects*:
* An address is removed from the `pendingAdmins` set for the account
* A `PendingAdminRemoved` event is emitted specifying the account for which a pending admin was removed

*Requirements*:
* The proposed admin MUST be a pending admin for the account
* Caller MUST be an admin for the account, or the account's address itself if no admin is set

#### `acceptAdmin`

```solidity
function acceptAdmin(address account) external;
```

To claim the admin role for an account, the pending admin must call `acceptAdmin()` passing in the relevant account.

Note that an account has successfully added an admin (i.e. the pending admin has called `acceptAdmin()`), **the account's address itself no longer has admin privileges**. This behavior benefits operators seeking to perform a *key rotation*, as adding an admin allows them to remove permissions from their original, potentially compromised, key. If an account wants to retain admin privileges for its own address, it is recommended to first add itself as an admin, then add any other admins as desired.

*Effects*:
* The caller is removed from the `pendingAdmins` set for the account
* The caller is added to the `admins` set for the account
* A `AdminSet` event is emitted specifying the account for which an admin was added

*Requirements*:
* Caller MUST be a pending admin for the account

### Removing an Admin

#### `removeAdmin`

```solidity
function removeAdmin(address account, address admin) external onlyAdmin(account);
```

An admin of an account can call `removeAdmin()` to remove any other admins of the same account. However, one admin must always remain for any given account. In other words, once an account has added an admin, it will always have at least one admin in perpetuity.

Note that once an admin has been added to an account, at least one must remain on the account.

*Effects*:
* The specified admin is removed from the `admins` set for the account
* An `AdminRemoved` event is emitted specifying the accuont for which an admin was removed

*Requirements*:
* `admins.length()` MUST be greater than 1, such that removing the admin does not remove all admins for the account
* The address to remove MUST be an admin for the account
* Caller MUST be an admin for the account, or the account's address itself if no admin is set

## Appointees

Appointees are able to act as another account *for a specific function or set of functions for a specific contract*, granting accounts granular access control.

### Adding an Appointee

#### `setAppointee`

```solidity
function setAppointee(
address account,
address appointee,
address target,
bytes4 selector
) external onlyAdmin(account);
```

An account (or its admins) can call `setAppointee()` to give another address the ability to call a specific function on a given contract. That address is then only able to call that specific function on that specific contract.

The `target` refers to the contract, and the `selector` to the function selector. Combined, these are the `targetSelector` and uniquely identify a permissioned function.

*Effects*:
* The `targetSelector` is added to the specified `appointee` set within the `appointeePermissions` mapping
* The `appointee` is added to the specified `targetSelector` set within the `permissionAppointees` mapping
* The `AppointeeSet` event is emitted, specifying the account, appointee, target contract, and function selector

*Requirements*:
* Caller MUST be an admin for the account, or the account's address itself if no admin is set
* The proposed appointee MUST NOT already have permissions for the given `targetSelector`

### Removing an Appointee

#### `removeAppointee`

```solidity
function removeAppointee(
address account,
address appointee,
address target,
bytes4 selector
) external onlyAdmin(account);
```

Similarly, an account (or its admins) can call `removeAppointee()` to remove that address's permissions for a given contract/function pair. Note that there does not exist any way currently to remove all permissions for a given appointee, or all appointees for a given function selector.

*Effects*:
* The `targetSelector` is removed from the specified `appointee` set within the `appointeePermissions` mapping
* The `appointee` is removed from the specified `targetSelector` set within the `permissionAppointees` mapping
* The `AppointeeRemoved` event is emitted, specifying the account, appointee, target contract, and function selector

*Requirements*:
* Caller MUST be an admin for the account, or the account's address itself if no admin is set
* The proposed appointee MUST already have permissions for the given `targetSelector`

0 comments on commit 766fe4a

Please sign in to comment.