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
}
}}
Submitted on: 2025-11-05 13:31:38
Comments
Log in to comment.
No comments yet.