Skip to content

Commit

Permalink
Merge pull request bgd-labs#36 from bgd-labs/feat/operational-payload…
Browse files Browse the repository at this point in the history
…s-controller

Feat/operational payloads controller
  • Loading branch information
Cycxyz authored Nov 22, 2024
2 parents 9e65335 + 0183572 commit 43fd72b
Show file tree
Hide file tree
Showing 10 changed files with 555 additions and 29 deletions.
Binary file added docs/permissioned-payloads-controller-flow.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions docs/permissioned-payloads-controller-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Permissioned payloads controller overview

[PermissionedPayloadsController](https://www.notion.so/src/contracts/payloads/PermissionedPayloadsController.sol): This is an extension of the PayloadsController contract, that modifies it so that only accepted actors (PayloadManager) can register a payload. The objective of having such extension is so that allowed third parties to affect change into low risk parts of the protocol, using similar processeces as governance. For that a new Executor contract will be registered to this Permissioned Payloads Controller, that will hold limited permissions.
At launch it will have permissions to change the LM programs.

**Key points and differences from PayloadsController**:

- Contract includes a payloads manager role with permission to create and cancel payloads.
- Payload creation is permissioned: only the payloads manager can create payloads.
- Contract has a guardian role to review created payloads and cancel them if incorrect or unrecognized.
- The guardian can adjust the timelock within certain limits.
- Payloads manager and guardian have equal permissions to cancel payloads. Payloads can be cancelled if not expired or executed.
- Payload execution is permissionless. If the timelock has passed and the payload isn't cancelled or expired, anyone can execute it. The Aave-robot is responsible for executing payloads.
- Executor holds permission emission admin role.

**The execution process operates as follows**:

1. A trusted entity creates a payload of any kind and submits it to the permissioned payloads controller.
2. Payload creation is combined with queuing: once created, a payload is automatically queued. The payload ID returned from the createPayload() function can be used to cancel or execute the payload.
3. The payload is assigned a timelock. During this timelock period, the guardian can verify the validity of the payload. If the payload is deemed invalid or unrecognized, the guardian can cancel it. The payloads manager also has the authority to cancel it.
4. Once the timelock period ends and the payload is unlocked and not expired, anyone is permitted to execute it.

![Permissioned payloads controller flow](./permissioned-payloads-controller-flow.jpg)
9 changes: 9 additions & 0 deletions docs/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@ Only one Governance must exist on the system (Ethereum mainnet), while multiple
- The Guardian can cancel a Payload if it has not been executed
- Payload State can’t decrease
- No further state transitions are possible if `proposal.state > 3`

## PermissionedPayloadsController
- Extension of PayloadsController
- Supports payloads only with Level_1 access level.
- Once executor config is created, it can't be changed except the delay configuration of the Level_1 executor.
- Only payloads manager is permitted to create a Payload.
- Payload creation is combined with queuing: once created, a Payload is automatically queued.
- Payloads manager can cancel a Payload as well as guardian.

5 changes: 4 additions & 1 deletion src/contracts/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,7 @@ library Errors {
string public constant CALLER_IS_NOT_VOTER_REPRESENTATIVE = '96'; // to represent a voter, caller must be the stored representative
string public constant VM_INVALID_GOVERNANCE_ADDRESS = '97'; // governance address can not be 0
string public constant ALL_DELEGATION_ACTIONS_FAILED = '98'; // all meta delegation actions failed on MetaDelegateHelper
}
string public constant ONLY_BY_PAYLOADS_MANAGER = '99'; // only payloads manager can call this function
string public constant ONLY_BY_PAYLOADS_MANAGER_OR_GUARDIAN = '100'; // only payloads manager or guardian can call this function
string public constant FUNCTION_NOT_SUPPORTED = '101'; // function not supported
}
71 changes: 43 additions & 28 deletions src/contracts/payloads/PayloadsControllerCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,29 @@ abstract contract PayloadsControllerCore is
using SafeCast for uint256;
using SafeERC20 for IERC20;

uint40 internal _payloadsCount;

// stores the executor configuration for every lvl of access control
mapping(PayloadsControllerUtils.AccessControl => ExecutorConfig)
internal _accessLevelToExecutorConfig;

mapping(uint40 => Payload) internal _payloads;

// should be always set with respect to the proposal flow duration
// for example: voting takes 5 days + 2 days for bridging + 3 days for cooldown + 2 days of safety gap
// then expirationDelay should be not less then 12 days.
// As expiration delay of proposal is 30 days, payload needs to be able to live longer as its created before and
// will be executed after.
// so nobody should be able to set expiration delay less then that
uint40 public constant EXPIRATION_DELAY = 35 days;

/// @inheritdoc IPayloadsControllerCore
uint40 public constant GRACE_PERIOD = 7 days;

uint40 internal _payloadsCount;

// stores the executor configuration for every lvl of access control
mapping(PayloadsControllerUtils.AccessControl => ExecutorConfig)
internal _accessLevelToExecutorConfig;
function EXPIRATION_DELAY() public pure virtual returns (uint40) {
return 35 days;
}

mapping(uint40 => Payload) internal _payloads;
/// @inheritdoc IPayloadsControllerCore
function GRACE_PERIOD() public pure virtual returns (uint40) {
return 7 days;
}

/// @inheritdoc IPayloadsControllerCore
function MIN_EXECUTION_DELAY() public view virtual returns (uint40) {
Expand All @@ -63,7 +68,7 @@ abstract contract PayloadsControllerCore is
address owner,
address guardian,
UpdateExecutorInput[] calldata executors
) external initializer {
) public virtual initializer {
require(executors.length != 0, Errors.SHOULD_BE_AT_LEAST_ONE_EXECUTOR);

_updateExecutors(executors);
Expand All @@ -75,7 +80,7 @@ abstract contract PayloadsControllerCore is
/// @inheritdoc IPayloadsControllerCore
function createPayload(
ExecutionAction[] calldata actions
) external returns (uint40) {
) public virtual returns (uint40) {
require(actions.length != 0, Errors.INVALID_EMPTY_TARGETS);

uint40 payloadId = _payloadsCount++;
Expand All @@ -84,8 +89,8 @@ abstract contract PayloadsControllerCore is
newPayload.creator = msg.sender;
newPayload.state = PayloadState.Created;
newPayload.createdAt = creationTime;
newPayload.expirationTime = creationTime + EXPIRATION_DELAY;
newPayload.gracePeriod = GRACE_PERIOD;
newPayload.expirationTime = creationTime + EXPIRATION_DELAY();
newPayload.gracePeriod = GRACE_PERIOD();

PayloadsControllerUtils.AccessControl maximumAccessLevelRequired;
for (uint256 i = 0; i < actions.length; i++) {
Expand Down Expand Up @@ -157,25 +162,16 @@ abstract contract PayloadsControllerCore is
}

/// @inheritdoc IPayloadsControllerCore
function cancelPayload(uint40 payloadId) external onlyGuardian {
Payload storage payload = _payloads[payloadId];

PayloadState payloadState = _getPayloadState(payload);
require(
payloadState < PayloadState.Executed &&
payloadState >= PayloadState.Created,
Errors.PAYLOAD_NOT_IN_THE_CORRECT_STATE
);
payload.state = PayloadState.Cancelled;
payload.cancelledAt = uint40(block.timestamp);

emit PayloadCancelled(payloadId);
function cancelPayload(
uint40 payloadId
) external virtual onlyGuardian {
_cancelPayload(payloadId);
}

/// @inheritdoc IPayloadsControllerCore
function updateExecutors(
UpdateExecutorInput[] calldata executors
) external onlyOwner {
) external virtual onlyOwner {
_updateExecutors(executors);
}

Expand Down Expand Up @@ -219,6 +215,25 @@ abstract contract PayloadsControllerCore is

receive() external payable {}

/**
* @notice method to cancel a payload
* @param payloadId id of the payload that needs to be canceled
*/
function _cancelPayload(uint40 payloadId) internal {
Payload storage payload = _payloads[payloadId];

PayloadState payloadState = _getPayloadState(payload);
require(
payloadState < PayloadState.Executed &&
payloadState >= PayloadState.Created,
Errors.PAYLOAD_NOT_IN_THE_CORRECT_STATE
);
payload.state = PayloadState.Cancelled;
payload.cancelledAt = uint40(block.timestamp);

emit PayloadCancelled(payloadId);
}

/**
* @notice method to get the current state of a payload
* @param payload object with all pertinent payload information
Expand Down
112 changes: 112 additions & 0 deletions src/contracts/payloads/PermissionedPayloadsController.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IPayloadsControllerCore} from './interfaces/IPayloadsControllerCore.sol';
import {PayloadsControllerCore} from './PayloadsControllerCore.sol';
import {PayloadsControllerUtils} from './PayloadsControllerUtils.sol';
import {WithPayloadsManager} from './WithPayloadsManager.sol';
import {IPermissionedPayloadsController} from './interfaces/IPermissionedPayloadsController.sol';
import {Errors} from '../libraries/Errors.sol';

/**
* @title PermissionedPayloadsController
* @author BGD Labs
* @notice this contract contains the logic to execute payloads
* without governance cycle but leaving the gap to review and cancel payloads.
* @dev this contract is permissioned, only the payloads manager can create
* and queue payloads. Also, not only guardian but also the payloads manager can cancel payloads.
* @dev constants were adjusted as the governance cycle is no longer needed.
* @dev owner and guardian are the same entity here.
*/
contract PermissionedPayloadsController is
PayloadsControllerCore,
WithPayloadsManager,
IPermissionedPayloadsController
{
/// @inheritdoc IPermissionedPayloadsController
function initialize(
address guardian,
address initialPayloadsManager,
UpdateExecutorInput[] calldata executors
)
public
override(PayloadsControllerCore, IPermissionedPayloadsController)
initializer
{
PayloadsControllerCore.initialize(guardian, guardian, executors);
_updatePayloadsManager(initialPayloadsManager);
}

/// @inheritdoc IPayloadsControllerCore
function MIN_EXECUTION_DELAY()
public
pure
override(PayloadsControllerCore, IPayloadsControllerCore)
returns (uint40)
{
return 0;
}

/// @inheritdoc IPayloadsControllerCore
function MAX_EXECUTION_DELAY()
public
pure
override(PayloadsControllerCore, IPayloadsControllerCore)
returns (uint40)
{
return 7 days;
}

/// @inheritdoc IPayloadsControllerCore
function cancelPayload(
uint40 payloadId
)
external
override(PayloadsControllerCore, IPayloadsControllerCore)
onlyPayloadsManagerOrGuardian
{
_cancelPayload(payloadId);
}

/// @inheritdoc IPayloadsControllerCore
function createPayload(
ExecutionAction[] calldata actions
)
public
override(PayloadsControllerCore, IPayloadsControllerCore)
onlyPayloadsManager
returns (uint40)
{
uint40 payloadId = super.createPayload(actions);
_queuePayload(
payloadId,
PayloadsControllerUtils.AccessControl.Level_1,
type(uint40).max
);
return payloadId;
}

/// @inheritdoc IPayloadsControllerCore
function updateExecutors(
UpdateExecutorInput[] calldata
)
external
pure
override(PayloadsControllerCore, IPayloadsControllerCore)
{
revert(Errors.FUNCTION_NOT_SUPPORTED);
}

/**
* @notice Sets the execution delay
* @param delay The new execution delay to be set
*/
function setExecutionDelay(uint40 delay) external onlyGuardian {
require(
delay >= MIN_EXECUTION_DELAY() && delay <= MAX_EXECUTION_DELAY(),
Errors.INVALID_EXECUTOR_DELAY
);
_accessLevelToExecutorConfig[PayloadsControllerUtils.AccessControl.Level_1]
.delay = delay;
}
}
57 changes: 57 additions & 0 deletions src/contracts/payloads/WithPayloadsManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IWithPayloadsManager} from './interfaces/IWithPayloadsManager.sol';
import {OwnableWithGuardian} from 'solidity-utils/contracts/access-control/OwnableWithGuardian.sol';
import {Errors} from '../libraries/Errors.sol';

/**
* @title WithPayloadsManager
* @author BGD Labs
* @dev Contract module which provides a basic access control mechanism, where
* there are accounts (owner, guardian, and payloads manager) which can be granted
* exclusive access to specific functions.
* @notice By default, all the roles will be assigned to the one that deploys the contract. This
* can later be changed with appropriate functions.
*/
contract WithPayloadsManager is OwnableWithGuardian, IWithPayloadsManager {
address private _payloadsManager;

constructor() {
_updatePayloadsManager(_msgSender());
}

modifier onlyPayloadsManager() {
require(_msgSender() == payloadsManager(), Errors.ONLY_BY_PAYLOADS_MANAGER);
_;
}

modifier onlyPayloadsManagerOrGuardian() {
require(
_msgSender() == payloadsManager() || _msgSender() == guardian(),
Errors.ONLY_BY_PAYLOADS_MANAGER_OR_GUARDIAN
);
_;
}

/// @inheritdoc IWithPayloadsManager
function payloadsManager() public view returns (address) {
return _payloadsManager;
}

/// @inheritdoc IWithPayloadsManager
function updatePayloadsManager(
address newPayloadsManager
) external onlyOwnerOrGuardian {
_updatePayloadsManager(newPayloadsManager);
}

/**
* @dev updates the address of the payloads manager
* @param newPayloadsManager the new address of the payloads manager.
*/
function _updatePayloadsManager(address newPayloadsManager) internal {
_payloadsManager = newPayloadsManager;
emit PayloadsManagerUpdated(newPayloadsManager);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IPayloadsControllerCore} from "./IPayloadsControllerCore.sol";
import {IWithPayloadsManager} from "./IWithPayloadsManager.sol";
import {PayloadsControllerUtils} from "../PayloadsControllerUtils.sol";

/**
* @title IPermissionedPayloadsController
* @author BGD Labs
* @notice interface containing the objects, events and methods definitions of the IPermissionedPayloadsController contract
*/
interface IPermissionedPayloadsController is IPayloadsControllerCore, IWithPayloadsManager {
/**
* @notice method to initialize the contract with starter params. Only callable by proxy
* @param guardian address of the guardian. With permissions to call certain methods
* @param initialPayloadsManager address of the initial payload manager
* @param executors array of executor configurations
*/
function initialize(
address guardian,
address initialPayloadsManager,
UpdateExecutorInput[] calldata executors
) external;

function setExecutionDelay(uint40 delay) external;
}
Loading

0 comments on commit 43fd72b

Please sign in to comment.