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;
}
}