Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 83 additions & 71 deletions contracts/account/modules/ERC7579DelayedExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ abstract contract ERC7579DelayedExecutor is ERC7579Executor {
/// @dev Emitted when the expiration delay is updated.
event ERC7579ExecutorExpirationUpdated(address indexed account, uint32 newExpiration);

/// @dev The module is not installed on the account.
error ERC7579ExecutorModuleNotInstalled();

/**
* @dev The current state of a operation is not the expected. The `expectedStates` is a bitmap with the
* bits enabled for each OperationState enum position counting from right to left. See {_encodeStateBitmap}.
Expand Down Expand Up @@ -122,72 +125,106 @@ abstract contract ERC7579DelayedExecutor is ERC7579Executor {
return OperationState.Ready;
}

/// @dev See {ERC7579Executor-canExecute}. Allows anyone to execute an operation if it's `Ready`.
function canExecute(
/**
* @dev See {ERC7579Executor-_validateExecutionRequest}. This implementation only validates
* the operation is in {OperationState.Ready}. Any caller can execute the operation if it is
* in this state.
*
* Derived contracts can override this function to add additional validation logic.
*
* Example extension:
*
* ```solidity
* function _validateExecutionRequest(
* address account,
* Mode mode,
* bytes calldata executionCalldata,
* bytes32 salt
* ) internal view virtual override {
* bool conditionMet = ...; // custom logic to check condition
* require(conditionMet, ERC7579ExecutorConditionNotMet());
* super._validateExecutionRequest(account, mode, executionCalldata, salt);
* }
*```
*/
function _validateExecutionRequest(
address account,
Mode mode,
bytes calldata executionCalldata,
bytes32 salt
) public view virtual override returns (bool) {
return
state(account, mode, executionCalldata, salt) == OperationState.Ready ||
super.canExecute(account, mode, executionCalldata, salt);
) internal view virtual override {
bytes32 id = hashOperation(account, mode, executionCalldata, salt);
_validateStateBitmap(id, _encodeStateBitmap(OperationState.Ready));
}

/**
* @dev Whether the caller is authorized to cancel operations.
* By default, this checks if the caller is the account itself. Derived contracts can
* override this to implement custom authorization logic.
* @dev Validates a cancellation request against required conditions. This base implementation
* validates the caller is authorized to cancel and that the operation is in a valid state.
*
* Derived contracts can override this function to add additional validation logic.
*
* Example extension:
*
* ```solidity
* function canCancel(
* function _validateCancelRequest(
* address account,
* Mode mode,
* bytes calldata executionCalldata,
* bytes32 salt
* ) public view virtual returns (bool) {
* bool isAuthorized = ...; // custom logic to check authorization
* return isAuthorized || super.canCancel(account, mode, executionCalldata, salt);
* ) internal view virtual override {
* bool conditionMet = ...; // custom logic to check additional condition
* require(conditionMet, ERC7579ExecutorConditionNotMet());
* super._validateCancelRequest(account, mode, executionCalldata, salt);
* }
*```
*/
function canCancel(
function _validateCancelRequest(
address account,
Mode /* mode */,
bytes calldata /* executionCalldata */,
bytes32 /* salt */
) public view virtual returns (bool) {
return account == msg.sender;
Mode mode,
bytes calldata executionCalldata,
bytes32 salt
) internal view virtual {
bytes32 id = hashOperation(account, mode, executionCalldata, salt);
bytes32 allowedStates = _encodeStateBitmap(OperationState.Scheduled) | _encodeStateBitmap(OperationState.Ready);
_validateStateBitmap(id, allowedStates);

require(msg.sender == account, ERC7579ExecutorUnauthorizedCancellation());
}

/**
* @dev Whether the caller is authorized to schedule operations.
* By default, this checks if the caller is the account itself. Derived contracts can
* override this to implement custom authorization logic.
* @dev Validates a schedule request. This base implementation validates
* the caller is authorized to schedule, the module is installed, and the
* operation was not scheduled before.
*
* Can be overridden by derived contracts to implement custom validation logic.
*
* Example extension:
*
* ```solidity
* function canSchedule(
* function _validateScheduleRequest(
* address account,
* Mode mode,
* bytes calldata executionCalldata,
* bytes32 salt
* ) public view virtual returns (bool) {
* bool isAuthorized = ...; // custom logic to check authorization
* return isAuthorized || super.canSchedule(account, mode, executionCalldata, salt);
* ) internal view virtual override {
* bool conditionMet = ...; // custom logic to check condition
* require(conditionMet, ERC7579ExecutorConditionNotMet());
* super._validateScheduleRequest(account, mode, executionCalldata, salt);
* }
*```
*/
function canSchedule(
function _validateScheduleRequest(
address account,
Mode /* mode */,
bytes calldata /* executionCalldata */,
bytes32 /* salt */
) public view virtual returns (bool) {
return account == msg.sender;
Mode mode,
bytes calldata executionCalldata,
bytes32 salt
) internal view virtual {
require(_config[account].installed, ERC7579ExecutorModuleNotInstalled());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would insist on keeping this case as a no-op. It's a valid use case to extend and allow scheduling on contracts that haven't installed the module. For example, a factory contract could deploy accounts with create2 and install this module with pre-scheduled payments (ie. subscriptions). In that case it becomes impossible to build that case on top of this contract


bytes32 id = hashOperation(account, mode, executionCalldata, salt);
_validateStateBitmap(id, _encodeStateBitmap(OperationState.Unknown));

require(msg.sender == account, ERC7579ExecutorUnauthorizedSchedule());
}

/// @dev Minimum delay after which {setDelay} takes effect.
Expand Down Expand Up @@ -293,22 +330,20 @@ abstract contract ERC7579DelayedExecutor is ERC7579Executor {
/**
* @dev Schedules an operation to be executed after the account's delay period (see {getDelay}).
* Operations are uniquely identified by the combination of `mode`, `executionCalldata`, and `salt`.
* See {canSchedule} for authorization checks.
* See {_validateScheduleRequest} for validation.
*/
function schedule(address account, Mode mode, bytes calldata executionCalldata, bytes32 salt) public virtual {
bool allowed = canSchedule(account, mode, executionCalldata, salt);
_schedule(account, mode, executionCalldata, salt); // Prioritize errors thrown in _schedule
require(allowed, ERC7579ExecutorUnauthorizedSchedule());
_validateScheduleRequest(account, mode, executionCalldata, salt);
_schedule(account, mode, executionCalldata, salt);
}

/**
* @dev Cancels a previously scheduled operation. Can only be called by the account that
* scheduled the operation. See {_cancel}.
* @dev Cancels a previously scheduled operation.
* See {_validateCancelRequest} for validation.
*/
function cancel(address account, Mode mode, bytes calldata executionCalldata, bytes32 salt) public virtual {
bool allowed = canCancel(account, mode, executionCalldata, salt);
_cancel(account, mode, executionCalldata, salt); // Prioritize errors thrown in _cancel
require(allowed, ERC7579ExecutorUnauthorizedCancellation());
_validateCancelRequest(account, mode, executionCalldata, salt);
_cancel(account, mode, executionCalldata, salt);
}

/**
Expand Down Expand Up @@ -354,42 +389,24 @@ abstract contract ERC7579DelayedExecutor is ERC7579Executor {
}

/**
* @dev Internal version of {schedule} that takes an `account` address as an argument.
*
* Requirements:
*
* * The operation must be `Unknown`.
* @dev Low-level internal function to schedule an operation. Does not perform any validation checks.
*
* Emits an {ERC7579ExecutorOperationScheduled} event.
*/
function _schedule(
address account,
Mode mode,
bytes calldata executionCalldata,
bytes32 salt
) internal virtual returns (bytes32 operationId, Schedule memory schedule_) {
function _schedule(address account, Mode mode, bytes calldata executionCalldata, bytes32 salt) internal virtual {
bytes32 id = hashOperation(account, mode, executionCalldata, salt);
_validateStateBitmap(id, _encodeStateBitmap(OperationState.Unknown));

(uint32 executableAfter, , ) = getDelay(account);

uint48 timepoint = Time.timestamp();

_schedules[id].scheduledAt = timepoint;
_schedules[id].executableAfter = executableAfter;
_schedules[id].expiresAfter = getExpiration(account);

emit ERC7579ExecutorOperationScheduled(account, id, mode, executionCalldata, salt, timepoint);
return (id, schedule_);
}

/**
* @dev See {ERC7579Executor-_execute}.
*
* Requirements:
*
* * The operation must be `Ready`.
*
* NOTE: Anyone can trigger execution once the operation is `Ready`. See {canExecute}.
* @inheritdoc ERC7579Executor
*/
function _execute(
address account,
Expand All @@ -398,26 +415,21 @@ abstract contract ERC7579DelayedExecutor is ERC7579Executor {
bytes32 salt
) internal virtual override returns (bytes[] memory returnData) {
bytes32 id = hashOperation(account, mode, executionCalldata, salt);
_validateStateBitmap(id, _encodeStateBitmap(OperationState.Ready));

_schedules[id].executed = true;

return super._execute(account, mode, executionCalldata, salt);
}

/**
* @dev Internal version of {cancel} that takes an `account` address as an argument.
*
* Requirements:
* @dev Low-level internal function to cancel an operation. Does not perform any validation checks.
*
* * The operation must be `Scheduled` or `Ready`.
* Note: Canceled operations can't be rescheduled.
*
* Canceled operations can't be rescheduled. Emits an {ERC7579ExecutorOperationCanceled} event.
* Emits an {ERC7579ExecutorOperationCanceled} event.
*/
function _cancel(address account, Mode mode, bytes calldata executionCalldata, bytes32 salt) internal virtual {
bytes32 id = hashOperation(account, mode, executionCalldata, salt);
bytes32 allowedStates = _encodeStateBitmap(OperationState.Scheduled) | _encodeStateBitmap(OperationState.Ready);
_validateStateBitmap(id, allowedStates);

_schedules[id].canceled = true;

Expand Down
35 changes: 17 additions & 18 deletions contracts/account/modules/ERC7579Executor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {Mode} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol
* The module enables accounts to execute arbitrary operations, leveraging the execution
* capabilities defined in the ERC-7579 standard. By default, the executor is restricted to
* operations initiated by the account itself, but this can be customized in derived contracts
* by overriding the {canExecute} function.
* by overriding the {_validateExecutionRequest} function.
*
* TIP: This is a simplified executor that directly executes operations without delay or expiration
* mechanisms. For a more advanced implementation with time-delayed execution patterns and
Expand All @@ -30,52 +30,51 @@ abstract contract ERC7579Executor is IERC7579Module {
}

/**
* @dev Check if the caller is authorized to execute operations.
* By default, this checks if the caller is the account itself. Derived contracts can
* override this to implement custom authorization logic.
* @dev Validates an execution request. By default, only validates the caller is the account itself.
* Derived contracts can override this function to add additional validation logic.
*
* Example extension:
*
* ```solidity
* function canExecute(
* function _validateExecutionRequest(
* address account,
* Mode mode,
* bytes calldata executionCalldata,
* bytes32 salt
* ) public view virtual returns (bool) {
* bool isAuthorized = ...; // custom logic to check authorization
* return isAuthorized || super.canExecute(account, mode, executionCalldata, salt);
* ) internal view virtual override {
* bool conditionMet = ...; // custom logic to check condition
* require(conditionMet, ERC7579ExecutorConditionNotMet());
* super._validateExecutionRequest(account, mode, executionCalldata, salt);
* }
*```
*/
function canExecute(
function _validateExecutionRequest(
address account,
Mode /* mode */,
bytes calldata /* executionCalldata */,
bytes32 /* salt */
) public view virtual returns (bool) {
return msg.sender == account;
) internal view virtual {
require(msg.sender == account, ERC7579UnauthorizedExecution());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would insist the issue with require statements is that developers can't remove them! The original canExecute intentionally returns bool to follow the pattern of condition || super.canExecute(), which allows to workaround a require(canSchedule(...)) error. I would rather keep that pattern in the new _validateExecutionRequest

Suggested change
) internal view virtual {
require(msg.sender == account, ERC7579UnauthorizedExecution());
) internal view virtual returns (bool) {
return msg.sender == account;

}

/**
* @dev Executes an operation and returns the result data from the executed operation.
* Restricted to the account itself by default. See {_execute} for requirements and
* {canExecute} for authorization checks.
* See {_execute} for requirements and {_validateExecutionRequest} for validation.
*/
function execute(
address account,
Mode mode,
bytes calldata executionCalldata,
bytes32 salt
) public virtual returns (bytes[] memory returnData) {
bool allowed = canExecute(account, mode, executionCalldata, salt);
returnData = _execute(account, mode, executionCalldata, salt); // Prioritize errors thrown in _execute
require(allowed, ERC7579UnauthorizedExecution());
return returnData;
_validateExecutionRequest(account, mode, executionCalldata, salt);
return _execute(account, mode, executionCalldata, salt);
}

/**
* @dev Internal version of {execute}. Emits {ERC7579ExecutorOperationExecuted} event.
* @dev Low-level internal function to execute an operation. Does not perform any validation checks.
*
* Emits {ERC7579ExecutorOperationExecuted} event.
*
* Requirements:
*
Expand Down
Loading
Loading