Sonic Blaze Testnet

Contract Diff Checker

Contract Name:
MultiSignatureWallet

Contract Source Code:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import {EnumerableSet} from "../lib/openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol";

/**
 * @title MultiSignatureWallet
 * @dev A multisignature wallet contract that requires multiple owners to confirm transactions.
 */
contract MultiSignatureWallet {
    using EnumerableSet for EnumerableSet.AddressSet;

    /**
     * @dev Emitted when a deposit is made to the contract.
     * @param sender The address that sent the funds.
     * @param amount The amount of funds deposited.
     * @param balance The new balance of the contract after the deposit.
     */
    event Deposit(address indexed sender, uint256 amount, uint256 balance);

    /**
     * @dev Emitted when a new transaction is submitted.
     * @param owner The address of the owner who submitted the transaction.
     * @param txIndex The index of the transaction in the transactions array.
     * @param to The contract address the transaction is directed to.
     * @param value The amount of ether to be sent with the transaction.
     * @param data The data payload of the transaction.
     */
    event SubmitTransaction(
        address indexed owner,
        uint256 indexed txIndex,
        address indexed to,
        uint256 value,
        bytes data
    );

    /**
     * @dev Emitted when a transaction is confirmed by an owner.
     * @param owner The address of the owner who confirmed the transaction.
     * @param txIndex The index of the transaction in the transactions array.
     */
    event ConfirmTransaction(address indexed owner, uint256 indexed txIndex);

    /**
     * @dev Emitted when a confirmation is revoked by an owner.
     * @param owner The address of the owner who revoked the confirmation.
     * @param txIndex The index of the transaction in the transactions array.
     */
    event RevokeConfirmation(address indexed owner, uint256 indexed txIndex);

    /**
     * @dev Emitted when a transaction is executed.
     * @param owner The address of the owner who executed the transaction.
     * @param txIndex The index of the transaction in the transactions array.
     * @param txData The data returned by the transaction call.
     */
    event ExecuteTransaction(address indexed owner, uint256 indexed txIndex, bytes txData);

    /**
     * @dev Emitted when new owners are added to the contract.
     * @param owners An array of addresses representing the newly added owners.
     */
    event OwnersAdded(address[] owners);

    /**
     * @dev Emitted when owners are removed from the contract.
     * @param owners An array of addresses representing the removed owners.
     */
    event OwnersRemoved(address[] owners);

    /**
     * @dev Emitted when the number of confirmations required is updated.
     * @param newNumConfirmation The new number of confirmations required for a transaction.
     */
    event NumConfirmationUpdated(uint256 newNumConfirmation);


    // Custom error definitions

    /**
     * @dev Error for when the function caller is not an owner.
     */
    error NotAnOwner();

    /**
     * @dev Error for when a transaction ID is invalid (e.g., out of bounds).
     */
    error InvalidTxnId();

    /**
     * @dev Error for when a transaction has already been executed.
     */
    error TxnAlreadyExecuted();

    /**
     * @dev Error for when a transaction has already been confirmed by the caller.
     */
    error TxnAlreadyConfirmed();

    /**
     * @dev Error for when the owners array is empty upon contract creation.
     */
    error OwnersRequired();

    /**
     * @dev Error for when the number of required confirmations is invalid (0 or more than the number of owners).
     */
    error InvalidNumberOfConfirmations();

    /**
     * @dev Error for when an invalid owner address is provided (e.g., zero address).
     */
    error InvalidOwner();

    /**
     * @dev Error for when a duplicate owner address is provided.
     */
    error OwnerNotUnique();

    /**
     * @dev Error for when a transaction does not have enough confirmations to be executed.
     */
    error NotEnoughConfirmation();

    /**
     * @dev Error for when a transaction has not been confirmed by the caller.
     */
    error TransactionNotConfirmed();

    /**
     * @dev Error for when a transaction has already expired.
     */
    error TransactionAlreadyExpired();

    /**
     * @dev Error for when a function is called by an account other than the multisig wallet itself.
     */
    error OnlyMultisigAccountCanCall();

    EnumerableSet.AddressSet private owners;
    uint256 public numConfirmationsRequired;

    // Structure to hold transaction details
    struct Transaction {
        address to; // Transaction target address
        bool executed; // Flag indicating if the transaction has been executed
        uint64 timeout; // Expiry timestamp of the transaction
        uint24 numConfirmations; // Number of confirmations received for the transaction
        uint256 value; // Amount of ether sent with the transaction
        bytes data; // Data payload of the transaction
    }

    // Mapping to track confirmations for each transaction by each owner
    mapping(uint256 transactionIndex => mapping(address owner => bool permissionToExecute)) public isConfirmed;

    // Array to store all transactions
    Transaction[] private transactions;

    // Function to ensure the caller is an owner
    function onlyOwner(address owner) private {
        if (!owners.contains(owner)) 
        revert NotAnOwner();
    }

    // Function to ensure the caller is the multisig contract itself
    function onlyMultiSig() private {
        if (msg.sender != address(this)) {
            revert OnlyMultisigAccountCanCall();
        }
    }

    // Function to check if a transaction exists
    function txExists(uint256 _txIndex) private {
        if (_txIndex >= transactions.length) 
        revert InvalidTxnId();
    }

    // Function to check if a transaction has not been executed
    function notExecuted(uint256 _txIndex) private {
        if (transactions[_txIndex].executed) 
        revert TxnAlreadyExecuted();
    }

    // Function to check if a transaction has not been expired or not
    function txNotExpired(uint256 _txIndex) private {
        if (transactions[_txIndex].timeout < block.timestamp)
            revert TransactionAlreadyExpired();
    }

    // Function to check if a transaction has not been confirmed by the caller
    function notConfirmed(uint256 _txIndex) private {
        if (isConfirmed[_txIndex][msg.sender])  revert TxnAlreadyConfirmed();
    }

    /**
     * @dev Constructor to initialize the contract with initial owners and required confirmations.
     * @param _owners Array of initial owner addresses.
     * @param _numConfirmationsRequired Number of confirmations required for transactions.
     */
    constructor(address[] memory _owners, uint256 _numConfirmationsRequired) {
        if (_owners.length == 0) revert OwnersRequired();
        if (
            _numConfirmationsRequired == 0 ||
            _numConfirmationsRequired > _owners.length
        ) revert InvalidNumberOfConfirmations();

        for (uint256 i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            if (owner == address(0)) revert InvalidOwner();
            if (owners.contains(owner)) revert OwnerNotUnique();
            owners.add(owner);
        }

        numConfirmationsRequired = _numConfirmationsRequired;
    }

    /**
     * @dev Fallback function to receive ether and emit a deposit event.
     */
    receive() external payable {
        emit Deposit(msg.sender, msg.value, address(this).balance);
    }

    /**
     * @dev Function to submit a new transaction to the wallet.
     * @param _to Address of the contract the transaction is directed to.
     * @param _value Amount of ether to be sent with the transaction.
     * @param _timeoutDuration Duration after which the transaction will get expire.
     * @param _data Data payload of the transaction.
     */
    function submitTransaction(
        address _to,
        uint256 _value,
        uint64 _timeoutDuration,
        bytes memory _data
    ) external payable {
        onlyOwner(msg.sender);
        uint256 txIndex = transactions.length;

        transactions.push(
            Transaction({
                to: _to,
                executed: false,
                timeout: uint64(block.timestamp) + _timeoutDuration,
               //We assume the act of submission is an implicit confirmation
                numConfirmations: 1,
                value: _value,
                data: _data
            })
        );

        isConfirmed[txIndex][msg.sender] = true;

        emit SubmitTransaction(msg.sender, txIndex, _to, _value, _data);
    }

    /**
     * @dev Function to confirm an existing transaction.
     * @param _txIndex Index of the transaction to confirm.
     */
    function confirmTransaction(uint256 _txIndex) public {
        onlyOwner(msg.sender);
        txExists(_txIndex);
        notExecuted(_txIndex);
        notConfirmed(_txIndex);
        txNotExpired(_txIndex);
        Transaction storage transaction = transactions[_txIndex];
        transaction.numConfirmations += 1;
        isConfirmed[_txIndex][msg.sender] = true;

        emit ConfirmTransaction(msg.sender, _txIndex);
    }

    /**
     * @dev Function to execute a confirmed transaction.
     * @param _txIndex Index of the transaction to execute.
     */
    function executeTransaction(uint256 _txIndex) public {
        onlyOwner(msg.sender);
        txExists(_txIndex);
        notExecuted(_txIndex);
        txNotExpired(_txIndex);
        Transaction storage transaction = transactions[_txIndex];
        if (transaction.numConfirmations < numConfirmationsRequired)
            revert NotEnoughConfirmation();
        transaction.executed = true;
        (bool success, bytes memory data) = transaction.to.call{
            value: transaction.value
        }(transaction.data);
        require(success, "tx failed");

        emit ExecuteTransaction(msg.sender, _txIndex, data);
    }

    /**
     * @dev Function to revoke a previously given confirmation for a transaction.
     * @param _txIndex Index of the transaction to revoke confirmation.
     */
    function revokeConfirmation(uint256 _txIndex) external {
        onlyOwner(msg.sender);
        txExists(_txIndex);
        notExecuted(_txIndex);
        txNotExpired(_txIndex);
        if (!isConfirmed[_txIndex][msg.sender]) {
            revert TransactionNotConfirmed();
        }

        Transaction storage transaction = transactions[_txIndex];

        transaction.numConfirmations -= 1;
        isConfirmed[_txIndex][msg.sender] = false;

        emit RevokeConfirmation(msg.sender, _txIndex);
    }

    /**
     * @dev Function to add new owners to the wallet.
     * @param _owners Array of new owner addresses to be added.
     */
    function addOwners(address[] memory _owners) public {
        onlyMultiSig();
        if (_owners.length == 0) revert OwnersRequired();

        address[] memory ownnersToUpdate = new address[](_owners.length);
        uint256 c = 0;

        for (uint256 i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            if (owner == address(0)) revert InvalidOwner();
            if (!owners.contains(owner)) {
                owners.add(owner);
                ownnersToUpdate[c++] = owner;
            }
        }
        if (c > 0)
        emit OwnersAdded(ownnersToUpdate);
    }

    /**
     * @dev Function to remove existing owners from the wallet.
     * @param _owners Array of existing owner addresses to be removed.
     */
    function removeOwners(address[] memory _owners) public {
        onlyMultiSig();
        if (_owners.length == 0) revert OwnersRequired();
        address[] memory ownnersToUpdate = new address[](_owners.length);
        uint256 c = 0;

        for (uint256 i = 0; i < _owners.length; i++) {
            address owner = _owners[i];
            if (owners.contains(owner)){
                owners.remove(owner);
                ownnersToUpdate[c++] = owner;
            }
        }

        if (owners.length() < numConfirmationsRequired) {
            revert InvalidNumberOfConfirmations();
        }

        if (c > 0)
        emit OwnersRemoved(ownnersToUpdate);
    }

    /**
     * @dev Function to update the number of required confirmations for transactions.
     * @param _numConfirmationsRequired New number of confirmations required for transactions.
     */
    function updateNumConfirmations(uint256 _numConfirmationsRequired) public {
        onlyMultiSig();
        if (
            _numConfirmationsRequired == 0 ||
            _numConfirmationsRequired > owners.length()
        ) revert InvalidNumberOfConfirmations();
        numConfirmationsRequired = _numConfirmationsRequired;
        emit NumConfirmationUpdated(_numConfirmationsRequired);
    }

    /**
     * @dev Function to retrieve the list of current owners of the wallet.
     * @return Array of addresses representing the current owners.
     */
    function getOwners() public view returns (address[] memory) {
        return owners.values();
    }

    /**
     * @dev Function to retrieve the count of transactions submitted to the wallet.
     * @return Total number of transactions in the wallet.
     */
    function getTransactionCount() public view returns (uint256) {
        return transactions.length;
    }

    /**
     * @dev Function to retrieve details of a specific transaction.
     * @param _txIndex Index of the transaction to retrieve details for.
     * @return to Transaction target address.
     * @return value Amount of ether sent with the transaction.
     * @return executed Boolean indicating if the transaction has been executed.
     * @return numConfirmations Number of confirmations received for the transaction.
     * @return timeout Expiry timestamp of the transaction.
     * @return data Data payload of the transaction.
     */
    function getTransaction(
        uint256 _txIndex
    )
        public
        view
        returns (
            address to,
            uint256 value,
            bool executed,
            uint256 numConfirmations,
            uint64 timeout,
            bytes memory data
        )
    {
        Transaction storage transaction = transactions[_txIndex];

        return (
            transaction.to,
            transaction.value,
            transaction.executed,
            transaction.numConfirmations,
            transaction.timeout,
            transaction.data
        );
    }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/EnumerableSet.sol)
// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.

pragma solidity ^0.8.0;

/**
 * @dev Library for managing
 * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
 * types.
 *
 * Sets have the following properties:
 *
 * - Elements are added, removed, and checked for existence in constant time
 * (O(1)).
 * - Elements are enumerated in O(n). No guarantees are made on the ordering.
 *
 * ```solidity
 * contract Example {
 *     // Add the library methods
 *     using EnumerableSet for EnumerableSet.AddressSet;
 *
 *     // Declare a set state variable
 *     EnumerableSet.AddressSet private mySet;
 * }
 * ```
 *
 * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)
 * and `uint256` (`UintSet`) are supported.
 *
 * [WARNING]
 * ====
 * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
 * unusable.
 * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
 *
 * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an
 * array of EnumerableSet.
 * ====
 */
library EnumerableSet {
    // To implement this library for multiple types with as little code
    // repetition as possible, we write it in terms of a generic Set type with
    // bytes32 values.
    // The Set implementation uses private functions, and user-facing
    // implementations (such as AddressSet) are just wrappers around the
    // underlying Set.
    // This means that we can only create new EnumerableSets for types that fit
    // in bytes32.

    struct Set {
        // Storage of set values
        bytes32[] _values;
        // Position of the value in the `values` array, plus 1 because index 0
        // means a value is not in the set.
        mapping(bytes32 => uint256) _indexes;
    }

    /**
     * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
    function _add(Set storage set, bytes32 value) private returns (bool) {
        if (!_contains(set, value)) {
            set._values.push(value);
            // The value is stored at length-1, but we add 1 to all indexes
            // and use 0 as a sentinel value
            set._indexes[value] = set._values.length;
            return true;
        } else {
            return false;
        }
    }

    /**
     * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
    function _remove(Set storage set, bytes32 value) private returns (bool) {
        // We read and store the value's index to prevent multiple reads from the same storage slot
        uint256 valueIndex = set._indexes[value];

        if (valueIndex != 0) {
            // Equivalent to contains(set, value)
            // To delete an element from the _values array in O(1), we swap the element to delete with the last one in
            // the array, and then remove the last element (sometimes called as 'swap and pop').
            // This modifies the order of the array, as noted in {at}.

            uint256 toDeleteIndex = valueIndex - 1;
            uint256 lastIndex = set._values.length - 1;

            if (lastIndex != toDeleteIndex) {
                bytes32 lastValue = set._values[lastIndex];

                // Move the last value to the index where the value to delete is
                set._values[toDeleteIndex] = lastValue;
                // Update the index for the moved value
                set._indexes[lastValue] = valueIndex; // Replace lastValue's index to valueIndex
            }

            // Delete the slot where the moved value was stored
            set._values.pop();

            // Delete the index for the deleted slot
            delete set._indexes[value];

            return true;
        } else {
            return false;
        }
    }

    /**
     * @dev Returns true if the value is in the set. O(1).
     */
    function _contains(Set storage set, bytes32 value) private view returns (bool) {
        return set._indexes[value] != 0;
    }

    /**
     * @dev Returns the number of values on the set. O(1).
     */
    function _length(Set storage set) private view returns (uint256) {
        return set._values.length;
    }

    /**
     * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
    function _at(Set storage set, uint256 index) private view returns (bytes32) {
        return set._values[index];
    }

    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function _values(Set storage set) private view returns (bytes32[] memory) {
        return set._values;
    }

    // Bytes32Set

    struct Bytes32Set {
        Set _inner;
    }

    /**
     * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
    function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
        return _add(set._inner, value);
    }

    /**
     * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
    function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
        return _remove(set._inner, value);
    }

    /**
     * @dev Returns true if the value is in the set. O(1).
     */
    function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
        return _contains(set._inner, value);
    }

    /**
     * @dev Returns the number of values in the set. O(1).
     */
    function length(Bytes32Set storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    /**
     * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
    function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
        return _at(set._inner, index);
    }

    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
        bytes32[] memory store = _values(set._inner);
        bytes32[] memory result;

        /// @solidity memory-safe-assembly
        assembly {
            result := store
        }

        return result;
    }

    // AddressSet

    struct AddressSet {
        Set _inner;
    }

    /**
     * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
    function add(AddressSet storage set, address value) internal returns (bool) {
        return _add(set._inner, bytes32(uint256(uint160(value))));
    }

    /**
     * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
    function remove(AddressSet storage set, address value) internal returns (bool) {
        return _remove(set._inner, bytes32(uint256(uint160(value))));
    }

    /**
     * @dev Returns true if the value is in the set. O(1).
     */
    function contains(AddressSet storage set, address value) internal view returns (bool) {
        return _contains(set._inner, bytes32(uint256(uint160(value))));
    }

    /**
     * @dev Returns the number of values in the set. O(1).
     */
    function length(AddressSet storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    /**
     * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
    function at(AddressSet storage set, uint256 index) internal view returns (address) {
        return address(uint160(uint256(_at(set._inner, index))));
    }

    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function values(AddressSet storage set) internal view returns (address[] memory) {
        bytes32[] memory store = _values(set._inner);
        address[] memory result;

        /// @solidity memory-safe-assembly
        assembly {
            result := store
        }

        return result;
    }

    // UintSet

    struct UintSet {
        Set _inner;
    }

    /**
     * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
    function add(UintSet storage set, uint256 value) internal returns (bool) {
        return _add(set._inner, bytes32(value));
    }

    /**
     * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
    function remove(UintSet storage set, uint256 value) internal returns (bool) {
        return _remove(set._inner, bytes32(value));
    }

    /**
     * @dev Returns true if the value is in the set. O(1).
     */
    function contains(UintSet storage set, uint256 value) internal view returns (bool) {
        return _contains(set._inner, bytes32(value));
    }

    /**
     * @dev Returns the number of values in the set. O(1).
     */
    function length(UintSet storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    /**
     * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
    function at(UintSet storage set, uint256 index) internal view returns (uint256) {
        return uint256(_at(set._inner, index));
    }

    /**
     * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
    function values(UintSet storage set) internal view returns (uint256[] memory) {
        bytes32[] memory store = _values(set._inner);
        uint256[] memory result;

        /// @solidity memory-safe-assembly
        assembly {
            result := store
        }

        return result;
    }
}

Please enter a contract address above to load the contract details and source code.

Context size (optional):