ExecutorFacet

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/Facets/ExecutorFacet.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.24;
import {IExecutorTypes} from "../Interfaces/IExecutorTypes.sol";
import {IAddressProviderService} from "../Interfaces/IAddressProviderService.sol";
import {IOwnershipFacet} from "../Interfaces/IOwnershipFacet.sol";
import {ThyraRegistry} from "../ThyraRegistry.sol";
import {IModuleManager} from "safe-smart-account/contracts/interfaces/IModuleManager.sol";
import {Enum} from "safe-smart-account/contracts/libraries/Enum.sol";

/// @title ExecutorFacet (Simplified)
/// @author Thyra.fi
/// @notice Simplified facet allowing whitelisted executors to execute any operation
/// @dev Executors must be registered in ThyraRegistry. No task registration required.
/// @custom:version 4.0.0-simplified
contract ExecutorFacet is IExecutorTypes, IAddressProviderService {
    /// @notice ThyraRegistry address for all ExecutorFacet instances
    /// @dev This address is set once during deployment and shared by all Diamond instances
    address public immutable THYRA_REGISTRY;

    /// @notice Constructor to set the ThyraRegistry address
    /// @param _thyraRegistry Address of the ThyraRegistry contract
    constructor(address _thyraRegistry) {
        THYRA_REGISTRY = _thyraRegistry;
    }

    /// Errors ///
    error UnauthorizedExecutor();
    error InvalidCallType();
    error ModuleExecutionFailed();

    /// Events ///
    event ExecutionSuccess(
        address indexed executor,
        address indexed target,
        uint256 value,
        bytes data,
        CallType callType
    );

    /// External Methods ///

    /**
     * @notice Execute any operation if caller is a whitelisted executor
     * @dev Validates executor against ThyraRegistry, then executes via Safe module
     * @param _operation Operation to execute
     * @return returnData Data returned by the executed transaction
     */
    function executeTransaction(Operation calldata _operation)
        external
        returns (bytes memory returnData)
    {
        // Validate executor is whitelisted in ThyraRegistry
        ThyraRegistry registry = ThyraRegistry(THYRA_REGISTRY);
        if (!registry.isExecutorWhitelisted(msg.sender)) {
            revert UnauthorizedExecutor();
        }

        // Validate call type - only CALL is supported
        if (_operation.callType != CallType.CALL) {
            revert InvalidCallType();
        }

        // Execute operation via Safe module
        returnData = _executeOperation(_operation);

        emit ExecutionSuccess(
            msg.sender, _operation.target, _operation.value, _operation.callData, _operation.callType
        );

        return returnData;
    }

    /**
     * @notice Get the ThyraRegistry contract address
     * @return The address of the ThyraRegistry contract
     */
    function getThyraRegistry() external view returns (address) {
        return THYRA_REGISTRY;
    }

    /// Internal Methods ///

    /**
     * @notice Execute operation through Safe wallet as module
     * @param _operation Operation to execute
     * @return returnData Data returned by execution
     */
    function _executeOperation(Operation memory _operation) internal returns (bytes memory returnData) {
        // Get Safe wallet address via OwnershipFacet interface
        // CRITICAL: We cannot use assembly sload(0) because ReentrancyGuard's _status 
        // variable also uses slot 0, which would conflict in delegatecall context
        address safeWallet = IOwnershipFacet(address(this)).safeWallet();

        // Execute transaction through Safe as module
        // CallType already validated, we only support CALL
        (bool success, bytes memory txnResult) = IModuleManager(safeWallet).execTransactionFromModuleReturnData(
            _operation.target, _operation.value, _operation.callData, Enum.Operation.Call
        );

        if (!success) {
            // Forward revert reason if available
            if (txnResult.length > 0) {
                // solhint-disable-next-line no-inline-assembly
                assembly {
                    revert(add(32, txnResult), mload(txnResult))
                }
            } else {
                revert ModuleExecutionFailed();
            }
        }

        return txnResult;
    }
}
"
    },
    "src/Interfaces/IExecutorTypes.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.17;

/// @title IExecutorTypes
/// @author ThyraWallet Team
/// @notice Type definitions for ExecutorFacet functionality with Merkle tree-based task execution
/// @custom:version 2.0.0
interface IExecutorTypes {
    /// @notice Types of calls that can be made
    enum CallType {
        CALL,
        DELEGATECALL
    }

    /// @notice Status of a task
    enum TaskStatus {
        INACTIVE, // Task is inactive (default state), operations cannot be executed
        ACTIVE, // Task is active and operations can be executed
        COMPLETED, // Task is completed, no more operations can be executed
        CANCELLED // Task is cancelled, no more operations can be executed

    }

    /**
     * @notice Data structure for a Merkle tree leaf node, representing a complete, verifiable operation
     * @param target Target contract address for this operation
     * @param value Amount of ETH to send with this call (msg.value)
     * @param callData Complete encoded function call data
     * @param callType Specifies whether this operation is CALL or DELEGATECALL
     * @param operationId Unique ID within current task (Merkle tree) to prevent replay attacks for non-repeatable operations
     * @param isRepeatable Flag indicating whether this operation can be executed multiple times
     * @param startTime Start timestamp when this operation can be executed (Unix timestamp)
     * @param endTime End timestamp when this operation can be executed (Unix timestamp)
     * @param maxGasPrice Maximum gas price the executor is willing to pay (wei)
     * @param gasLimit Maximum gas amount this operation can consume
     * @param gasToken ERC20 token address used for gas payment (address(0) means native ETH)
     */
    struct Operation {
        // Core Execution Payload
        address target;
        uint256 value;
        bytes callData;
        CallType callType;
        // Security & Validation Parameters
        uint32 operationId;
        bool isRepeatable;
        uint32 startTime;
        uint32 endTime;
        uint256 maxGasPrice;
        uint256 gasLimit;
        address gasToken;
    }

    /**
     * @notice EIP712 execution parameters structure for signing
     * @param operation Operation type as uint8 (0=CALL, 1=DELEGATECALL)
     * @param to Target contract address
     * @param account Account address performing the operation
     * @param executor Authorized executor address
     * @param value Amount of ETH to send
     * @param nonce Execution nonce for replay protection
     * @param data Call data to execute
     */
    struct ExecutionParams {
        uint8 operation;
        address to;
        address account;
        address executor;
        uint256 value;
        uint256 nonce;
        bytes data;
    }
}
"
    },
    "src/Interfaces/IAddressProviderService.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.17;

/// @title IAddressProviderService
/// @author Thyra.fi
/// @notice Interface for providing global service addresses to Diamond facets
/// @custom:version 1.0.0
interface IAddressProviderService {
    /// @notice Get the ThyraRegistry contract address
    /// @return The address of the ThyraRegistry contract
    function getThyraRegistry() external view returns (address);
}
"
    },
    "src/Interfaces/IOwnershipFacet.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.17;

import {IERC173} from "./IERC173.sol";

/// @title Interface for OwnershipFacet (Extended)
/// @author ThyraWallet Team
/// @notice Extended ownership interface including Safe wallet initialization
/// @custom:version 1.0.0
interface IOwnershipFacet is IERC173 {
    /// @notice Initialize Diamond with factory and Safe wallet
    /// @param _factory Address of the Factory that deployed this Diamond
    /// @param _safeWallet Address of the Safe wallet
    function initialize(address _factory, address _safeWallet) external;

    /// @notice Get the Safe wallet address
    /// @return Safe wallet address (zero address if not initialized)
    function safeWallet() external view returns (address);

    /// @notice Get the factory address
    /// @return Factory address that deployed this Diamond
    function factory() external view returns (address);
}

"
    },
    "src/ThyraRegistry.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.17;

/// @title ThyraRegistry
/// @author Thyra.fi
/// @notice Global configuration and whitelist registry for the Thyra ecosystem
/// @dev This contract serves as the single source of truth for global configurations,
///      fee token whitelists, executor whitelists, and fee configurations across
///      all Thyra Diamond contracts and future facets.
/// @custom:version 1.0.0
contract ThyraRegistry {
    /// @notice Contract owner address
    address public owner;

    /// @notice Mapping of fee tokens to their whitelist status
    /// @dev ERC20 tokens that are approved for use as payment for transaction fees
    mapping(address => bool) public isFeeTokenWhitelisted;

    /// @notice Mapping of executors to their whitelist status
    /// @dev Globally approved executor addresses that can execute operations
    mapping(address => bool) public isExecutorWhitelisted;

    /// @notice Fee configuration structure
    /// @dev Defines the minimum and maximum fees for each whitelisted fee token
    struct FeeConfig {
        uint96 minFee; // Minimum fee amount (in token's smallest unit)
        uint96 maxFee; // Maximum fee amount (in token's smallest unit)
    }

    /// @notice Mapping of fee tokens to their fee configurations
    /// @dev Stores the min/max fee bounds for each whitelisted fee token
    mapping(address => FeeConfig) public feeTokenConfigs;

    /// @notice Events
    event FeeTokenWhitelistChanged(address indexed token, bool isWhitelisted);
    event ExecutorWhitelistChanged(address indexed executor, bool isWhitelisted);
    event FeeConfigChanged(address indexed token, uint96 minFee, uint96 maxFee);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /// @notice Errors
    error OnlyOwner();
    error InvalidFeeBounds();
    error TokenNotWhitelisted();
    error ZeroAddress();
    error ExecutorNotWhitelisted();
    error FeeTokenNotWhitelisted();
    error InvalidFeeRange();

    /// @notice Modifier to restrict function access to contract owner only
    modifier onlyOwner() {
        if (msg.sender != owner) revert OnlyOwner();
        _;
    }

    /// @notice Contract constructor
    /// @dev Initializes the contract with the deployer as the initial owner
    constructor() {
        owner = msg.sender;
    }

    /// @notice Set the whitelist status of a fee token
    /// @dev Only the contract owner can add or remove fee tokens from the whitelist
    /// @param _token The ERC20 token address to update
    /// @param _isWhitelisted True to add to whitelist, false to remove
    function setFeeToken(address _token, bool _isWhitelisted) external onlyOwner {
        if (_token == address(0)) revert ZeroAddress();

        isFeeTokenWhitelisted[_token] = _isWhitelisted;
        emit FeeTokenWhitelistChanged(_token, _isWhitelisted);
    }

    /// @notice Set the whitelist status of an executor
    /// @dev Only the contract owner can add or remove executors from the global whitelist
    /// @param _executor The executor address to update
    /// @param _isWhitelisted True to add to whitelist, false to remove
    function setExecutor(address _executor, bool _isWhitelisted) external onlyOwner {
        if (_executor == address(0)) revert ZeroAddress();

        isExecutorWhitelisted[_executor] = _isWhitelisted;
        emit ExecutorWhitelistChanged(_executor, _isWhitelisted);
    }

    /// @notice Set the fee configuration for a whitelisted token
    /// @dev Only the contract owner can set fee configurations, and only for whitelisted tokens
    /// @param _token The ERC20 token address to configure
    /// @param _minFee The minimum fee amount (must be <= maxFee)
    /// @param _maxFee The maximum fee amount (must be >= minFee)
    function setFeeConfig(address _token, uint96 _minFee, uint96 _maxFee) external onlyOwner {
        if (_token == address(0)) revert ZeroAddress();
        if (_minFee > _maxFee) revert InvalidFeeBounds();
        if (!isFeeTokenWhitelisted[_token]) revert TokenNotWhitelisted();

        feeTokenConfigs[_token] = FeeConfig({minFee: _minFee, maxFee: _maxFee});

        emit FeeConfigChanged(_token, _minFee, _maxFee);
    }

    /// @notice Transfer ownership of the contract to a new address
    /// @dev Only the current owner can transfer ownership
    /// @param _newOwner The address of the new owner (cannot be zero address)
    function transferOwnership(address _newOwner) external onlyOwner {
        if (_newOwner == address(0)) revert ZeroAddress();

        address previousOwner = owner;
        owner = _newOwner;

        emit OwnershipTransferred(previousOwner, _newOwner);
    }

    /// @notice Validate task registration parameters against global configuration
    /// @dev Centralized validation logic for task registration across all Diamond contracts
    /// @param _executor Executor address to validate
    /// @param _feeToken Fee token address to validate
    /// @param _initFee Initial fee amount to validate
    /// @param _maxFee Maximum fee amount to validate
    function validateTaskRegistration(address _executor, address _feeToken, uint96 _initFee, uint96 _maxFee)
        external
        view
    {
        // Validate zero addresses
        if (_executor == address(0) || _feeToken == address(0)) {
            revert ZeroAddress();
        }

        // Check executor whitelist
        if (!isExecutorWhitelisted[_executor]) {
            revert ExecutorNotWhitelisted();
        }

        // Check fee token whitelist
        if (!isFeeTokenWhitelisted[_feeToken]) {
            revert FeeTokenNotWhitelisted();
        }

        // Validate fee range against registry config
        FeeConfig memory config = feeTokenConfigs[_feeToken];
        if (
            _initFee < config.minFee || _initFee > config.maxFee || _maxFee < config.minFee || _maxFee > config.maxFee
                || _initFee > _maxFee
        ) {
            revert InvalidFeeRange();
        }
    }
}
"
    },
    "lib/safe-smart-account/contracts/interfaces/IModuleManager.sol": {
      "content": "// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
import {Enum} from "../libraries/Enum.sol";

/**
 * @title IModuleManager - An interface of contract managing Safe modules
 * @notice Modules are extensions with unlimited access to a Safe that can be added to a Safe by its owners.
           ⚠️ WARNING: Modules are a security risk since they can execute arbitrary transactions, 
           so only trusted and audited modules should be added to a Safe. A malicious module can
           completely takeover a Safe.
 * @author @safe-global/safe-protocol
 */
interface IModuleManager {
    event EnabledModule(address indexed module);
    event DisabledModule(address indexed module);
    event ExecutionFromModuleSuccess(address indexed module);
    event ExecutionFromModuleFailure(address indexed module);
    event ChangedModuleGuard(address indexed moduleGuard);

    /**
     * @notice Enables the module `module` for the Safe.
     * @dev This can only be done via a Safe transaction.
     * @param module Module to be whitelisted.
     */
    function enableModule(address module) external;

    /**
     * @notice Disables the module `module` for the Safe.
     * @dev This can only be done via a Safe transaction.
     * @param prevModule Previous module in the modules linked list.
     * @param module Module to be removed.
     */
    function disableModule(address prevModule, address module) external;

    /**
     * @notice Execute `operation` (0: Call, 1: DelegateCall) to `to` with `value` (Native Token)
     * @param to Destination address of module transaction.
     * @param value Ether value of module transaction.
     * @param data Data payload of module transaction.
     * @param operation Operation type of module transaction.
     * @return success Boolean flag indicating if the call succeeded.
     */
    function execTransactionFromModule(
        address to,
        uint256 value,
        bytes memory data,
        Enum.Operation operation
    ) external returns (bool success);

    /**
     * @notice Execute `operation` (0: Call, 1: DelegateCall) to `to` with `value` (Native Token) and return data
     * @param to Destination address of module transaction.
     * @param value Ether value of module transaction.
     * @param data Data payload of module transaction.
     * @param operation Operation type of module transaction.
     * @return success Boolean flag indicating if the call succeeded.
     * @return returnData Data returned by the call.
     */
    function execTransactionFromModuleReturnData(
        address to,
        uint256 value,
        bytes memory data,
        Enum.Operation operation
    ) external returns (bool success, bytes memory returnData);

    /**
     * @notice Returns if a module is enabled
     * @return True if the module is enabled
     */
    function isModuleEnabled(address module) external view returns (bool);

    /**
     * @notice Returns an array of modules.
     *         If all entries fit into a single page, the next pointer will be 0x1.
     *         If another page is present, next will be the last element of the returned array.
     * @param start Start of the page. Has to be a module or start pointer (0x1 address)
     * @param pageSize Maximum number of modules that should be returned. Has to be > 0
     * @return array Array of modules.
     * @return next Start of the next page.
     */
    function getModulesPaginated(address start, uint256 pageSize) external view returns (address[] memory array, address next);

    /**
     * @dev Set a module guard that checks transactions initiated by the module before execution
     *      This can only be done via a Safe transaction.
     *      ⚠️ IMPORTANT: Since a module guard has full power to block Safe transaction execution initiated via a module,
     *        a broken module guard can cause a denial of service for the Safe modules. Make sure to carefully
     *        audit the module guard code and design recovery mechanisms.
     * @notice Set Module Guard `moduleGuard` for the Safe. Make sure you trust the module guard.
     * @param moduleGuard The address of the module guard to be used or the zero address to disable the module guard.
     */
    function setModuleGuard(address moduleGuard) external;
}
"
    },
    "lib/safe-smart-account/contracts/libraries/Enum.sol": {
      "content": "// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

/**
 * @title Enum - Collection of enums used in Safe Smart Account contracts.
 * @author @safe-global/safe-protocol
 */
library Enum {
    enum Operation {
        Call,
        DelegateCall
    }
}
"
    },
    "src/Interfaces/IERC173.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.17;

/// @title Interface for ERC-173 (Contract Ownership Standard)
/// @author LI.FI (https://li.fi)
/// Note: the ERC-165 identifier for this interface is 0x7f5828d0
/// @custom:version 1.0.0
interface IERC173 {
    /// @dev This emits when ownership of a contract changes.
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /// @notice Get the address of the owner
    /// @return owner_ The address of the owner.
    function owner() external view returns (address owner_);

    /// @notice Set the address of the new owner of the contract
    /// @dev Set _newOwner to address(0) to renounce any ownership.
    /// @param _newOwner The address of the new owner of the contract
    function transferOwnership(address _newOwner) external;
}
"
    }
  },
  "settings": {
    "remappings": [
      "openzeppelin-contracts/=lib/openzeppelin-contracts/",
      "safe-smart-account/=lib/safe-smart-account/",
      "forge-std/=lib/forge-std/src/",
      "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
      "erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/",
      "halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "evmVersion": "cancun",
    "viaIR": true
  }
}}

Tags:
Multisig, Upgradeable, Multi-Signature, Factory|addr:0x4c25a744403d574c0a746951a17b2817be2ace7f|verified:true|block:23731837|tx:0x934f8e923d38005062a69b857df5ce9458e937f8493f4c9cff54e8ac9ce73ee2|first_check:1762345911

Submitted on: 2025-11-05 13:31:53

Comments

Log in to comment.

No comments yet.