// Source: contracts/governance/Multisig.sol pragma solidity ^0.8.0; // SPDX-License-Identifier: MIT // File contracts/interfaces/IBaseMultisig.sol /** * @title IMultisigBase Interface * @notice An interface defining the base operations for a multi-signature contract. */ interface IBaseMultisig { error NotSigner(); error AlreadyVoted(); error InvalidSigners(); error InvalidSignerThreshold(); error DuplicateSigner(address account); /**********\ |* Events *| \**********/ event MultisigVoted( bytes32 indexed topic, uint256 indexed signerEpoch, address indexed voter, uint256 voteCount, uint256 threshold ); event MultisigOperationExecuted( bytes32 indexed topic, uint256 indexed signerEpoch, address indexed voter, uint256 threshold ); event SignersRotated(address[] newAccounts, uint256 newThreshold); /***********\ |* Getters *| \***********/ /** * @notice Gets the current epoch. * @return uint The current epoch */ function signerEpoch() external view returns (uint256); /** * @notice Gets the threshold of current signers. * @return uint The threshold number */ function signerThreshold() external view returns (uint256); /** * @notice Gets the array of current signers. * @return array of signer addresses */ function signerAccounts() external view returns (address[] memory); /** * @notice Getter to determine if an account is a signer * @return boolean indicating if the account is a signer */ function isSigner(address account) external view returns (bool); /** * @notice Getter to determine if an account has voted on a topic * @return boolean indicating if the account has voted */ function hasSignerVoted(address account, bytes32 topic) external view returns (bool); /** * @notice Get the number of votes for a topic * @return uint256 indicating the number of votes for a topic */ function getSignerVotesCount(bytes32 topic) external view returns (uint256); /***********\ |* Setters *| \***********/ /** * @notice Update the signers and threshold for the multisig contract. * @param newAccounts The array of new signers * @param newThreshold The new threshold of signers required */ function rotateSigners(address[] memory newAccounts, uint256 newThreshold) external; } // File contracts/governance/BaseMultisig.sol /** * @title MultisigBase Contract * @notice This contract implements a custom multi-signature wallet where transactions must be confirmed by a * threshold of signers. The signers and threshold may be updated every `epoch`. */ contract BaseMultisig is IBaseMultisig { struct Voting { uint256 voteCount; mapping(address => bool) hasVoted; } struct Signers { address[] accounts; uint256 threshold; mapping(address => bool) isSigner; } Signers public signers; uint256 public signerEpoch; // uint256 is for epoch, bytes32 for vote topic hash mapping(uint256 => mapping(bytes32 => Voting)) public votingPerTopic; /** * @notice Contract constructor * @dev Sets the initial list of signers and corresponding threshold. * @param accounts Address array of the signers * @param threshold Signature threshold required to validate a transaction */ constructor(address[] memory accounts, uint256 threshold) { _rotateSigners(accounts, threshold); } /** * @notice Modifier to ensure the caller is a signer * @dev Keeps track of votes for each operation and resets the vote count if the operation is executed. * @dev Given the early void return, this modifier should be used with care on functions that return data. */ modifier onlySigners() { if (!_isFinalSignerVote()) return; _; } /******************\ |* Public Getters *| \******************/ /** * @notice Returns the current signer threshold * @return uint256 The signer threshold */ function signerThreshold() external view override returns (uint256) { return signers.threshold; } /** * @notice Returns an array of current signers * @return array of signer addresses */ function signerAccounts() external view override returns (address[] memory) { return signers.accounts; } /** * @notice Getter to determine if an account is a signer * @return boolean indicating if the account is a signer */ function isSigner(address account) external view override returns (bool) { return signers.isSigner[account]; } /** * @notice Getter to determine if an account has voted on a topic * @return boolean indicating if the account has voted */ function hasSignerVoted(address account, bytes32 topic) external view override returns (bool) { return votingPerTopic[signerEpoch][topic].hasVoted[account]; } /** * @notice Get the number of votes for a topic * @return uint256 indicating the number of votes for a topic */ function getSignerVotesCount(bytes32 topic) external view override returns (uint256) { return votingPerTopic[signerEpoch][topic].voteCount; } /***********\ |* Setters *| \***********/ /** * @notice Rotate the signers for the multisig * @dev Updates the current set of signers and threshold and increments the `epoch` * @dev This function is protected by the onlySigners modifier * @param newAccounts Address array of the new signers * @param newThreshold The new signature threshold for executing operations */ function rotateSigners(address[] memory newAccounts, uint256 newThreshold) external virtual onlySigners { _rotateSigners(newAccounts, newThreshold); } /** * @dev Internal function that implements signer rotation logic */ function _rotateSigners(address[] memory newAccounts, uint256 newThreshold) internal { uint256 length = signers.accounts.length; // Clean up old signers. for (uint256 i; i < length; ++i) { delete signers.isSigner[signers.accounts[i]]; } length = newAccounts.length; if (newThreshold > length) revert InvalidSigners(); if (newThreshold == 0) revert InvalidSignerThreshold(); ++signerEpoch; signers.accounts = newAccounts; signers.threshold = newThreshold; for (uint256 i; i < length; ++i) { address account = newAccounts[i]; // Check that the account wasn't already set as a signer for this epoch. if (signers.isSigner[account]) revert DuplicateSigner(account); if (account == address(0)) revert InvalidSigners(); signers.isSigner[account] = true; } emit SignersRotated(newAccounts, newThreshold); } /** * @dev Internal function that implements onlySigners logic */ function _isFinalSignerVote() internal returns (bool) { if (!signers.isSigner[msg.sender]) revert NotSigner(); bytes32 topic = keccak256(msg.data); uint256 epoch = signerEpoch; Voting storage voting = votingPerTopic[epoch][topic]; // Check that signer has not voted, then record that they have voted. if (voting.hasVoted[msg.sender]) revert AlreadyVoted(); voting.hasVoted[msg.sender] = true; // Determine the new vote count. uint256 voteCount = voting.voteCount + 1; uint256 threshold = signers.threshold; // Do not proceed with operation execution if insufficient votes. if (voteCount < threshold) { emit MultisigVoted(topic, epoch, msg.sender, voteCount, threshold); // Save updated vote count. voting.voteCount = voteCount; return false; } emit MultisigOperationExecuted(topic, epoch, msg.sender, threshold); // Clear vote count and voted booleans. _resetVoting(voting); return true; } /** * @dev Internal function to reset the votes for a topic */ function _resetSignerVotes(bytes32 topic) internal { _resetVoting(votingPerTopic[signerEpoch][topic]); } /** * @dev Internal function to reset the votes for a topic */ function _resetVoting(Voting storage voting) internal { delete voting.voteCount; uint256 count = signers.accounts.length; for (uint256 i; i < count; ++i) { delete voting.hasVoted[signers.accounts[i]]; } } } // File contracts/interfaces/ICaller.sol interface ICaller { error InvalidContract(address target); error InsufficientBalance(); error ExecutionFailed(); } // File contracts/interfaces/IContractExecutor.sol /** * @title IContractExecutor Interface * @notice This interface defines the execute function used to interact with external contracts. */ interface IContractExecutor { /** * @notice Executes a call to an external contract. * @dev Execution logic is left up to the implementation. * @param target The address of the contract to be called * @param callData The calldata to be sent * @param nativeValue The amount of native token (e.g., Ether) to be sent along with the call * @return bytes The data returned from the executed call */ function executeContract( address target, bytes calldata callData, uint256 nativeValue ) external payable returns (bytes memory); } // File contracts/interfaces/IMultisig.sol /** * @title IMultisig Interface * @notice This interface extends IMultisigBase by adding an execute function for multisignature transactions. */ interface IMultisig is ICaller, IContractExecutor, IBaseMultisig { /** * @notice Withdraws native token from the contract * @param recipient The address to send the native token to * @param amount The amount of native token to send * @dev This function is only callable by the contract itself after passing according proposal */ function withdraw(address recipient, uint256 amount) external; } // File contracts/libs/SafeNativeTransfer.sol error NativeTransferFailed(); /* * @title SafeNativeTransfer * @dev This library is used for performing safe native value transfers in Solidity by utilizing inline assembly. */ library SafeNativeTransfer { /* * @notice Perform a native transfer to a given address. * @param receiver The recipient address to which the amount will be sent. * @param amount The amount of native value to send. * @throws NativeTransferFailed error if transfer is not successful. */ function safeNativeTransfer(address receiver, uint256 amount) internal { bool success; assembly { success := call(gas(), receiver, amount, 0, 0, 0, 0) } if (!success) revert NativeTransferFailed(); } } // File contracts/libs/ContractAddress.sol library ContractAddress { function isContract(address contractAddress) internal view returns (bool) { bytes32 existingCodeHash = contractAddress.codehash; // https://eips.ethereum.org/EIPS/eip-1052 // keccak256('') == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 return existingCodeHash != bytes32(0) && existingCodeHash != 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; } } // File contracts/utils/Caller.sol contract Caller is ICaller { using ContractAddress for address; /** * @dev Calls a target address with specified calldata and optionally sends value. */ function _call( address target, bytes calldata callData, uint256 nativeValue ) internal returns (bytes memory) { if (!target.isContract()) revert InvalidContract(target); if (nativeValue > address(this).balance) revert InsufficientBalance(); (bool success, bytes memory data) = target.call{ value: nativeValue }(callData); if (!success) { revert ExecutionFailed(); } return data; } } // File contracts/governance/Multisig.sol /** * @title Multisig Contract * @notice An extension of MultisigBase that can call functions on any contract. */ contract Multisig is Caller, BaseMultisig, IMultisig { using SafeNativeTransfer for address; /** * @notice Contract constructor * @dev Sets the initial list of signers and corresponding threshold. * @param accounts Address array of the signers * @param threshold Signature threshold required to validate a transaction */ constructor(address[] memory accounts, uint256 threshold) BaseMultisig(accounts, threshold) {} /** * @notice Executes an external contract call. * @notice This function is protected by the onlySigners requirement. * @dev Calls a target address with specified calldata and passing provided native value. * @param target The address of the contract to call * @param callData The data encoding the function and arguments to call * @param nativeValue The amount of native currency (e.g., ETH) to send along with the call * @return data return data from executed function call */ function executeContract( address target, bytes calldata callData, uint256 nativeValue ) external payable returns (bytes memory) { if (!_isFinalSignerVote()) return bytes(''); return _call(target, callData, nativeValue); } /** * @notice Withdraws native token from the contract. * @notice This function is protected by the onlySigners modifier. * @param recipient The address to send the native token to * @param amount The amount of native token to send * @dev This function is only callable by the contract itself after passing according proposal */ function withdraw(address recipient, uint256 amount) external onlySigners { recipient.safeNativeTransfer(amount); } /** * @notice Making contact able to receive native value */ receive() external payable {} }