Skip to content

Commit

Permalink
add simple executor (#103)
Browse files Browse the repository at this point in the history
* add simple executor

* action as free level export
  • Loading branch information
novaknole authored Sep 27, 2024
1 parent 7c4dff4 commit 717d1ba
Show file tree
Hide file tree
Showing 15 changed files with 189 additions and 92 deletions.
39 changes: 0 additions & 39 deletions contracts/src/dao/IDAO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,6 @@ pragma solidity ^0.8.8;
/// @notice The interface required for DAOs within the Aragon App DAO framework.
/// @custom:security-contact [email protected]
interface IDAO {
/// @notice The action struct to be consumed by the DAO's `execute` function resulting in an external call.
/// @param to The address to call.
/// @param value The native token value to be sent with the call.
/// @param data The bytes-encoded function selector and calldata for the call.
struct Action {
address to;
uint256 value;
bytes data;
}

/// @notice Checks if an address has permission on a contract via a permission identifier and considers if `ANY_ADDRESS` was used in the granting process.
/// @param _where The address of the contract.
/// @param _who The address of a EOA or contract to give the permissions.
Expand All @@ -38,35 +28,6 @@ interface IDAO {
/// @param metadata The IPFS hash of the new metadata object.
event MetadataSet(bytes metadata);

/// @notice Executes a list of actions. If a zero allow-failure map is provided, a failing action reverts the entire execution. If a non-zero allow-failure map is provided, allowed actions can fail without the entire call being reverted.
/// @param _callId The ID of the call. The definition of the value of `callId` is up to the calling contract and can be used, e.g., as a nonce.
/// @param _actions The array of actions.
/// @param _allowFailureMap A bitmap allowing execution to succeed, even if individual actions might revert. If the bit at index `i` is 1, the execution succeeds even if the `i`th action reverts. A failure map value of 0 requires every action to not revert.
/// @return The array of results obtained from the executed actions in `bytes`.
/// @return The resulting failure map containing the actions have actually failed.
function execute(
bytes32 _callId,
Action[] memory _actions,
uint256 _allowFailureMap
) external returns (bytes[] memory, uint256);

/// @notice Emitted when a proposal is executed.
/// @param actor The address of the caller.
/// @param callId The ID of the call.
/// @param actions The array of actions executed.
/// @param allowFailureMap The allow failure map encoding which actions are allowed to fail.
/// @param failureMap The failure map encoding which actions have failed.
/// @param execResults The array with the results of the executed actions.
/// @dev The value of `callId` is defined by the component/contract calling the execute function. A `Plugin` implementation can use it, for example, as a nonce.
event Executed(
address indexed actor,
bytes32 callId,
Action[] actions,
uint256 allowFailureMap,
uint256 failureMap,
bytes[] execResults
);

/// @notice Emitted when a standard callback is registered.
/// @param interfaceId The ID of the interface.
/// @param callbackSelector The selector of the callback function.
Expand Down
86 changes: 86 additions & 0 deletions contracts/src/executors/Executor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

import {IExecutor, Action} from "./IExecutor.sol";
import {flipBit, hasBit} from "../utils/math/BitMap.sol";

/// @notice Simple Executor that loops through the actions and executes them.
/// @dev Reverts in case enough gas was not provided for the last action.
contract Executor is IExecutor {
/// @notice The internal constant storing the maximal action array length.
uint256 internal constant MAX_ACTIONS = 256;

/// @notice Thrown if the action array length is larger than `MAX_ACTIONS`.
error TooManyActions();

/// @notice Thrown if an action has insufficient gas left.
error InsufficientGas();

/// @notice Thrown if action execution has failed.
/// @param index The index of the action in the action array that failed.
error ActionFailed(uint256 index);

/// @inheritdoc IExecutor
function execute(
bytes32 _callId,
Action[] memory _actions,
uint256 _allowFailureMap
) public virtual override returns (bytes[] memory execResults, uint256 failureMap) {
// Check that the action array length is within bounds.
if (_actions.length > MAX_ACTIONS) {
revert TooManyActions();
}

execResults = new bytes[](_actions.length);

uint256 gasBefore;
uint256 gasAfter;

for (uint256 i = 0; i < _actions.length; ) {
gasBefore = gasleft();

(bool success, bytes memory data) = _actions[i].to.call{value: _actions[i].value}(
_actions[i].data
);

gasAfter = gasleft();

// Check if failure is allowed
if (!hasBit(_allowFailureMap, uint8(i))) {
// Check if the call failed.
if (!success) {
revert ActionFailed(i);
}
} else {
// Check if the call failed.
if (!success) {
// Make sure that the action call did not fail because 63/64 of `gasleft()` was insufficient to execute the external call `.to.call` (see [ERC-150](https://eips.ethereum.org/EIPS/eip-150)).
// In specific scenarios, i.e. proposal execution where the last action in the action array is allowed to fail, the account calling `execute` could force-fail this action by setting a gas limit
// where 63/64 is insufficient causing the `.to.call` to fail, but where the remaining 1/64 gas are sufficient to successfully finish the `execute` call.
if (gasAfter < gasBefore / 64) {
revert InsufficientGas();
}

// Store that this action failed.
failureMap = flipBit(failureMap, uint8(i));
}
}

execResults[i] = data;

unchecked {
++i;
}
}

emit Executed({
actor: msg.sender,
callId: _callId,
actions: _actions,
allowFailureMap: _allowFailureMap,
failureMap: failureMap,
execResults: execResults
});
}
}
50 changes: 50 additions & 0 deletions contracts/src/executors/IExecutor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

pragma solidity ^0.8.8;

import {IDAO} from "../dao/IDAO.sol";

/// @notice The action struct to be consumed by the DAO's `execute` function resulting in an external call.
/// @param to The address to call.
/// @param value The native token value to be sent with the call.
/// @param data The bytes-encoded function selector and calldata for the call.
struct Action {
address to;
uint256 value;
bytes data;
}

/// @title IDAO
/// @author Aragon X - 2022-2023
/// @notice The interface required for Executors within the Aragon App DAO framework.
/// @custom:security-contact [email protected]
interface IExecutor {
/// @notice Emitted when a proposal is executed.
/// @param actor The address of the caller.
/// @param callId The ID of the call.
/// @param actions The array of actions executed.
/// @param allowFailureMap The allow failure map encoding which actions are allowed to fail.
/// @param failureMap The failure map encoding which actions have failed.
/// @param execResults The array with the results of the executed actions.
/// @dev The value of `callId` is defined by the component/contract calling the execute function. A `Plugin` implementation can use it, for example, as a nonce.
event Executed(
address indexed actor,
bytes32 callId,
Action[] actions,
uint256 allowFailureMap,
uint256 failureMap,
bytes[] execResults
);

/// @notice Executes a list of actions. If a zero allow-failure map is provided, a failing action reverts the entire execution. If a non-zero allow-failure map is provided, allowed actions can fail without the entire call being reverted.
/// @param _callId The ID of the call. The definition of the value of `callId` is up to the calling contract and can be used, e.g., as a nonce.
/// @param _actions The array of actions.
/// @param _allowFailureMap A bitmap allowing execution to succeed, even if individual actions might revert. If the bit at index `i` is 1, the execution succeeds even if the `i`th action reverts. A failure map value of 0 requires every action to not revert.
/// @return The array of results obtained from the executed actions in `bytes`.
/// @return The resulting failure map containing the actions have actually failed.
function execute(
bytes32 _callId,
Action[] memory _actions,
uint256 _allowFailureMap
) external returns (bytes[] memory, uint256);
}
3 changes: 2 additions & 1 deletion contracts/src/mocks/dao/DAOMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
pragma solidity ^0.8.8;

import {IDAO} from "../../dao/IDAO.sol";
import {IExecutor, Action} from "../../executors/IExecutor.sol";

/// @notice A mock DAO that anyone can set permissions in.
/// @dev DO NOT USE IN PRODUCTION!
contract DAOMock is IDAO {
contract DAOMock is IDAO, IExecutor {
bool public hasPermissionReturnValueMock;

function setHasPermissionReturnValueMock(bool _hasPermissionReturnValueMock) external {
Expand Down
16 changes: 4 additions & 12 deletions contracts/src/mocks/plugin/CustomExecutorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,18 @@
pragma solidity ^0.8.8;

import {IDAO} from "../../dao/IDAO.sol";
import {IExecutor, Action} from "../../executors/IExecutor.sol";

/// @notice A mock DAO that anyone can set permissions in.
/// @dev DO NOT USE IN PRODUCTION!
contract CustomExecutorMock {
contract CustomExecutorMock is IExecutor {
error Failed();

event Executed(
address indexed actor,
bytes32 callId,
IDAO.Action[] actions,
uint256 allowFailureMap,
uint256 failureMap,
bytes[] execResults
);

function execute(
bytes32 callId,
IDAO.Action[] memory _actions,
Action[] memory _actions,
uint256 allowFailureMap
) external returns (bytes[] memory execResults, uint256 failureMap) {
) external override returns (bytes[] memory execResults, uint256 failureMap) {
if (callId == bytes32(0)) {
revert Failed();
} else if (callId == bytes32(uint256(123))) {
Expand Down
5 changes: 3 additions & 2 deletions contracts/src/mocks/plugin/PluginCloneableMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity ^0.8.8;

import {PluginCloneable} from "../../plugin/PluginCloneable.sol";
import {IDAO} from "../../dao/IDAO.sol";
import {IExecutor, Action} from "../../executors/IExecutor.sol";

/// @notice A mock cloneable plugin to be deployed via the minimal proxy pattern.
/// v1.1 (Release 1, Build 1)
Expand All @@ -19,7 +20,7 @@ contract PluginCloneableMockBuild1 is PluginCloneable {

function execute(
uint256 _callId,
IDAO.Action[] memory _actions,
Action[] memory _actions,
uint256 _allowFailureMap
) external returns (bytes[] memory execResults, uint256 failureMap) {
(execResults, failureMap) = _execute(bytes32(_callId), _actions, _allowFailureMap);
Expand All @@ -28,7 +29,7 @@ contract PluginCloneableMockBuild1 is PluginCloneable {
function execute(
address _target,
uint256 _callId,
IDAO.Action[] memory _actions,
Action[] memory _actions,
uint256 _allowFailureMap,
Operation _op
) external returns (bytes[] memory execResults, uint256 failureMap) {
Expand Down
5 changes: 3 additions & 2 deletions contracts/src/mocks/plugin/PluginMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.8;

import {Plugin} from "../../plugin/Plugin.sol";
import {IDAO} from "../../dao/IDAO.sol";
import {IExecutor, Action} from "../../executors/IExecutor.sol";

/// @notice A mock plugin to be deployed via the `new` keyword.
/// v1.1 (Release 1, Build 1)
Expand All @@ -17,7 +18,7 @@ contract PluginMockBuild1 is Plugin {

function execute(
uint256 _callId,
IDAO.Action[] memory _actions,
Action[] memory _actions,
uint256 _allowFailureMap
) external returns (bytes[] memory execResults, uint256 failureMap) {
(execResults, failureMap) = _execute(bytes32(_callId), _actions, _allowFailureMap);
Expand All @@ -26,7 +27,7 @@ contract PluginMockBuild1 is Plugin {
function execute(
address _target,
uint256 _callId,
IDAO.Action[] memory _actions,
Action[] memory _actions,
uint256 _allowFailureMap,
Operation _op
) external returns (bytes[] memory execResults, uint256 failureMap) {
Expand Down
5 changes: 3 additions & 2 deletions contracts/src/mocks/plugin/PluginUUPSUpgradeableMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity ^0.8.8;

import {PluginUUPSUpgradeable} from "../../plugin/PluginUUPSUpgradeable.sol";
import {IDAO} from "../../dao/IDAO.sol";
import {IExecutor, Action} from "../../executors/IExecutor.sol";

/// @notice A mock upgradeable plugin to be deployed via the UUPS proxy pattern.
/// v1.1 (Release 1, Build 1)
Expand All @@ -19,7 +20,7 @@ contract PluginUUPSUpgradeableMockBuild1 is PluginUUPSUpgradeable {

function execute(
uint256 _callId,
IDAO.Action[] memory _actions,
Action[] memory _actions,
uint256 _allowFailureMap
) external returns (bytes[] memory execResults, uint256 failureMap) {
(execResults, failureMap) = _execute(bytes32(_callId), _actions, _allowFailureMap);
Expand All @@ -28,7 +29,7 @@ contract PluginUUPSUpgradeableMockBuild1 is PluginUUPSUpgradeable {
function execute(
address _target,
uint256 _callId,
IDAO.Action[] memory _actions,
Action[] memory _actions,
uint256 _allowFailureMap,
Operation _op
) external returns (bytes[] memory execResults, uint256 failureMap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pragma solidity ^0.8.8;

import {Proposal} from "../../../../plugin/extensions/proposal/Proposal.sol";
import {IDAO} from "../../../../dao/IDAO.sol";
import {IExecutor, Action} from "../../../../executors/IExecutor.sol";

/// @notice A mock contract.
/// @dev DO NOT USE IN PRODUCTION!
Expand All @@ -14,7 +14,7 @@ contract ProposalMock is Proposal {
// solhint-disable no-empty-blocks
function createProposal(
bytes memory data,
IDAO.Action[] memory actions,
Action[] memory actions,
uint64 startDate,
uint64 endDate,
bytes memory
Expand All @@ -23,7 +23,7 @@ contract ProposalMock is Proposal {
function canExecute(uint256 proposalId) external view returns (bool) {}

function createProposalId(
IDAO.Action[] memory actions,
Action[] memory actions,
bytes memory metadata
) external view returns (uint256) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pragma solidity ^0.8.8;

import {ProposalUpgradeable} from "../../../../plugin/extensions/proposal/ProposalUpgradeable.sol";
import {IDAO} from "../../../../dao/IDAO.sol";
import {IExecutor, Action} from "../../../../executors/IExecutor.sol";

/// @notice A mock contract.
/// @dev DO NOT USE IN PRODUCTION!
Expand All @@ -14,7 +14,7 @@ contract ProposalUpgradeableMock is ProposalUpgradeable {
// solhint-disable no-empty-blocks
function createProposal(
bytes memory data,
IDAO.Action[] memory actions,
Action[] memory actions,
uint64 startDate,
uint64 endDate,
bytes memory
Expand All @@ -23,7 +23,7 @@ contract ProposalUpgradeableMock is ProposalUpgradeable {
function canExecute(uint256 proposalId) external view returns (bool) {}

function createProposalId(
IDAO.Action[] memory actions,
Action[] memory actions,
bytes memory metadata
) external view returns (uint256) {}

Expand Down
Loading

0 comments on commit 717d1ba

Please sign in to comment.