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": {
"@openzeppelin/contracts/access/Ownable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
"
},
"@openzeppelin/contracts/utils/Context.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
"
},
"contracts/utils/MultiSig.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;
/**
* @title MultiSig
* @dev Multi-signature contract that automatically allows registered contracts from Registry
* Supports multiple pending transactions from different contracts simultaneously
*/
contract MultiSig {
// =============================================================
// STRUCTS
// =============================================================
struct Transaction {
address targetContract; // Contract to call
bytes data; // Function call data
uint256 approvals; // Current approval count
bool executed; // Execution status
address proposer; // Who proposed the transaction
uint256 timestamp; // When proposed
string description; // Human readable description
}
// =============================================================
// STATE VARIABLES
// =============================================================
/// @notice Array of the 5 authorized signers
address[5] public signers;
/// @notice Maps transaction ID to transaction details
mapping(uint256 => Transaction) public transactions;
/// @notice Maps transaction ID to signer address to approval status
mapping(uint256 => mapping(address => bool)) public hasApproved;
/// @notice Next transaction ID to use
uint256 public nextTransactionId;
/// @notice Minimum approvals required (3 out of 5)
uint256 public constant REQUIRED_APPROVALS = 3;
/// @notice Reference to the Registry contract
address public registry;
/// @notice Maps contract addresses that can delegate to this Multi-Sig
mapping(address => bool) public authorizedContracts;
// =============================================================
// EVENTS
// =============================================================
/**
* @notice Emitted when a transaction is proposed
* @param txId The unique transaction ID
* @param targetContract The contract the transaction will call
* @param proposer The address that proposed the transaction
* @param description Human readable description
*/
event TransactionProposed(
uint256 indexed txId,
address indexed targetContract,
address indexed proposer,
string description
);
/**
* @notice Emitted when a signer approves a transaction
* @param txId The transaction ID
* @param signer The address of the approving signer
* @param approvalCount Current approval count
*/
event TransactionApproved(
uint256 indexed txId,
address indexed signer,
uint256 approvalCount
);
/**
* @notice Emitted when a transaction is executed
* @param txId The transaction ID
* @param executor The address that executed the transaction
* @param targetContract The contract that was called
*/
event TransactionExecuted(
uint256 indexed txId,
address indexed executor,
address indexed targetContract
);
/**
* @notice Emitted when a contract is authorized to use Multi-Sig
* @param contractAddress The address of the authorized contract
* @param authorized Whether the contract is authorized
*/
event ContractAuthorized(
address indexed contractAddress,
bool authorized
);
/**
* @notice Emitted when signers are updated
* @param oldSigners The previous array of signers
* @param newSigners The new array of signers
*/
event SignersUpdated(
address[5] oldSigners,
address[5] newSigners
);
// =============================================================
// MODIFIERS
// =============================================================
/**
* @dev Modifier to check if the caller is one of the authorized signers
*/
modifier onlySigner() {
require(isSigner(msg.sender), "MultiSig: Not authorized signer");
_;
}
/**
* @dev Modifier to check if the caller is an authorized contract
*/
modifier onlyAuthorizedContract() {
require(_isAuthorizedContract(msg.sender), "MultiSig: Not authorized contract");
_;
}
/**
* @dev Modifier to check if transaction exists and hasn't been executed
*/
modifier validTransaction(uint256 txId) {
require(txId < nextTransactionId, "MultiSig: Transaction does not exist");
require(!transactions[txId].executed, "MultiSig: Transaction already executed");
_;
}
// =============================================================
// CONSTRUCTOR
// =============================================================
/**
* @notice Initialize the multi-sig with 5 authorized signers and registry
* @param _signer1 First authorized signer address
* @param _signer2 Second authorized signer address
* @param _signer3 Third authorized signer address
* @param _signer4 Fourth authorized signer address
* @param _signer5 Fifth authorized signer address
* @param _registry Address of the Registry contract
*/
constructor(
address _signer1,
address _signer2,
address _signer3,
address _signer4,
address _signer5,
address _registry
) {
require(_signer1 != address(0), "MultiSig: Invalid signer1");
require(_signer2 != address(0), "MultiSig: Invalid signer2");
require(_signer3 != address(0), "MultiSig: Invalid signer3");
require(_signer4 != address(0), "MultiSig: Invalid signer4");
require(_signer5 != address(0), "MultiSig: Invalid signer5");
require(_signer1 != _signer2 && _signer1 != _signer3 && _signer1 != _signer4 && _signer1 != _signer5 &&
_signer2 != _signer3 && _signer2 != _signer4 && _signer2 != _signer5 &&
_signer3 != _signer4 && _signer3 != _signer5 &&
_signer4 != _signer5,
"MultiSig: Duplicate signers not allowed");
signers[0] = _signer1;
signers[1] = _signer2;
signers[2] = _signer3;
signers[3] = _signer4;
signers[4] = _signer5;
registry = _registry;
nextTransactionId = 1; // Start from 1 to avoid confusion with default values
}
// =============================================================
// ADMIN FUNCTIONS
// =============================================================
/**
* @notice Authorize a contract to delegate transactions to this Multi-Sig
* @param contractAddress The address of the contract to authorize
* @param authorized Whether to authorize or deauthorize
*/
function setAuthorizedContract(address contractAddress, bool authorized) external onlySigner {
require(contractAddress != address(0), "MultiSig: Invalid contract address");
require(contractAddress != address(this), "MultiSig: Cannot authorize self");
// This needs Multi-Sig approval too
bytes memory data = abi.encodeWithSignature("_setAuthorizedContract(address,bool)", contractAddress, authorized);
uint256 txId = _createTransaction(
address(this),
data,
string(abi.encodePacked("Authorize contract: ", _addressToString(contractAddress)))
);
// Auto-approve from the proposer
hasApproved[txId][msg.sender] = true;
transactions[txId].approvals++;
emit TransactionProposed(txId, address(this), msg.sender, string(abi.encodePacked("Authorize contract: ", _addressToString(contractAddress))));
emit TransactionApproved(txId, msg.sender, transactions[txId].approvals);
}
/**
* @dev Internal function to actually set authorized contract (called after Multi-Sig approval)
*/
function _setAuthorizedContract(address contractAddress, bool authorized) external {
require(msg.sender == address(this), "MultiSig: Only self can call");
authorizedContracts[contractAddress] = authorized;
emit ContractAuthorized(contractAddress, authorized);
}
/**
* @notice Propose to replace the current signers with new ones (requires Multi-Sig approval)
* @param _newSigner1 New first signer address
* @param _newSigner2 New second signer address
* @param _newSigner3 New third signer address
* @param _newSigner4 New fourth signer address
* @param _newSigner5 New fifth signer address
*/
function proposeSignerReplacement(
address _newSigner1,
address _newSigner2,
address _newSigner3,
address _newSigner4,
address _newSigner5
) external onlySigner {
require(_newSigner1 != address(0), "MultiSig: Invalid signer1");
require(_newSigner2 != address(0), "MultiSig: Invalid signer2");
require(_newSigner3 != address(0), "MultiSig: Invalid signer3");
require(_newSigner4 != address(0), "MultiSig: Invalid signer4");
require(_newSigner5 != address(0), "MultiSig: Invalid signer5");
require(_newSigner1 != _newSigner2 && _newSigner1 != _newSigner3 && _newSigner1 != _newSigner4 && _newSigner1 != _newSigner5 &&
_newSigner2 != _newSigner3 && _newSigner2 != _newSigner4 && _newSigner2 != _newSigner5 &&
_newSigner3 != _newSigner4 && _newSigner3 != _newSigner5 &&
_newSigner4 != _newSigner5,
"MultiSig: Duplicate signers not allowed");
bytes memory data = abi.encodeWithSignature(
"_replaceSigners(address,address,address,address,address)",
_newSigner1,
_newSigner2,
_newSigner3,
_newSigner4,
_newSigner5
);
uint256 txId = _createTransaction(
address(this),
data,
string(abi.encodePacked(
"Replace signers with: ",
_addressToString(_newSigner1), ", ",
_addressToString(_newSigner2), ", ",
_addressToString(_newSigner3), ", ",
_addressToString(_newSigner4), ", ",
_addressToString(_newSigner5)
))
);
// Auto-approve from the proposer
hasApproved[txId][msg.sender] = true;
transactions[txId].approvals++;
emit TransactionProposed(
txId,
address(this),
msg.sender,
"Replace current signers"
);
emit TransactionApproved(txId, msg.sender, transactions[txId].approvals);
}
/**
* @dev Internal function to actually replace signers (called after Multi-Sig approval)
*/
function _replaceSigners(
address _newSigner1,
address _newSigner2,
address _newSigner3,
address _newSigner4,
address _newSigner5
) external {
require(msg.sender == address(this), "MultiSig: Only self can call");
address[5] memory oldSigners = signers;
signers[0] = _newSigner1;
signers[1] = _newSigner2;
signers[2] = _newSigner3;
signers[3] = _newSigner4;
signers[4] = _newSigner5;
emit SignersUpdated(oldSigners, signers);
}
// =============================================================
// DELEGATION FUNCTIONS
// =============================================================
/**
* @notice Called by authorized contracts to delegate a transaction to Multi-Sig
* @param targetContract The contract to call after approval
* @param data The function call data
* @param description Human readable description of the transaction
* @return txId The unique transaction ID for tracking
*/
function delegateTransaction(
address targetContract,
bytes calldata data,
string calldata description
) external onlyAuthorizedContract returns (uint256 txId) {
require(targetContract != address(0), "MultiSig: Invalid target contract");
require(data.length > 0, "MultiSig: Empty transaction data");
require(bytes(description).length > 0, "MultiSig: Empty description");
txId = _createTransaction(targetContract, data, description);
emit TransactionProposed(txId, targetContract, msg.sender, description);
return txId;
}
// =============================================================
// APPROVAL FUNCTIONS
// =============================================================
/**
* @notice Approve a pending transaction
* @param txId The transaction ID to approve
*/
function approveTransaction(uint256 txId) external onlySigner validTransaction(txId) {
require(!hasApproved[txId][msg.sender], "MultiSig: Already approved");
hasApproved[txId][msg.sender] = true;
transactions[txId].approvals++;
emit TransactionApproved(txId, msg.sender, transactions[txId].approvals);
// Auto-execute if we have enough approvals
if (transactions[txId].approvals >= REQUIRED_APPROVALS) {
_executeTransaction(txId);
}
}
/**
* @notice Execute a transaction that has received sufficient approvals
* @param txId The transaction ID to execute
*/
function executeTransaction(uint256 txId) external onlySigner validTransaction(txId) {
require(transactions[txId].approvals >= REQUIRED_APPROVALS, "MultiSig: Insufficient approvals");
_executeTransaction(txId);
}
// =============================================================
// INTERNAL FUNCTIONS
// =============================================================
/**
* @dev Internal function to create a new transaction
*/
function _createTransaction(
address targetContract,
bytes memory data,
string memory description
) internal returns (uint256 txId) {
txId = nextTransactionId++;
transactions[txId] = Transaction({
targetContract: targetContract,
data: data,
approvals: 0,
executed: false,
proposer: msg.sender,
timestamp: block.timestamp,
description: description
});
return txId;
}
/**
* @dev Internal function to check if a contract is authorized
* Checks manual authorization, Registry registration, and allows Registry itself
*/
function _isAuthorizedContract(address contractAddress) internal view returns (bool) {
// Check manual authorization first
if (authorizedContracts[contractAddress]) {
return true;
}
// Allow Registry itself to call
if (registry != address(0) && contractAddress == registry) {
return true;
}
// Check Registry registration if registry is set
if (registry != address(0)) {
// Call the Registry's isRegistered function
(bool success, bytes memory data) = registry.staticcall(
abi.encodeWithSignature("isRegistered(address)", contractAddress)
);
if (success && data.length == 32) {
return abi.decode(data, (bool));
}
}
return false;
}
/**
* @dev Internal function to execute a transaction
*/
function _executeTransaction(uint256 txId) internal {
Transaction storage txn = transactions[txId];
txn.executed = true;
(bool success, bytes memory returnData) = txn.targetContract.call(txn.data);
require(success, string(abi.encodePacked("MultiSig: Transaction failed: ", string(returnData))));
emit TransactionExecuted(txId, msg.sender, txn.targetContract);
}
/**
* @dev Convert address to string for descriptions
*/
function _addressToString(address addr) internal pure returns (string memory) {
bytes32 value = bytes32(uint256(uint160(addr)));
bytes memory alphabet = "0123456789abcdef";
bytes memory str = new bytes(42);
str[0] = '0';
str[1] = 'x';
for (uint256 i = 0; i < 20; i++) {
str[2+i*2] = alphabet[uint8(value[i + 12] >> 4)];
str[3+i*2] = alphabet[uint8(value[i + 12] & 0x0f)];
}
return string(str);
}
// =============================================================
// READ FUNCTIONS
// =============================================================
/**
* @notice Check if an address is one of the authorized signers
* @param _address The address to check
* @return True if the address is a signer, false otherwise
*/
function isSigner(address _address) public view returns (bool) {
for (uint256 i = 0; i < 5; i++) {
if (signers[i] == _address) {
return true;
}
}
return false;
}
/**
* @notice Get transaction details
* @param txId The transaction ID
* @return Transaction details
*/
function getTransaction(uint256 txId) external view returns (Transaction memory) {
require(txId < nextTransactionId, "MultiSig: Transaction does not exist");
return transactions[txId];
}
/**
* @notice Check if a signer has approved a specific transaction
* @param txId The transaction ID
* @param signer The address of the signer to check
* @return True if the signer has approved, false otherwise
*/
function hasSignerApproved(uint256 txId, address signer) external view returns (bool) {
return hasApproved[txId][signer];
}
/**
* @notice Get all pending transactions (not executed)
* @return Array of transaction IDs that are pending
*/
function getPendingTransactions() external view returns (uint256[] memory) {
uint256[] memory pending = new uint256[](nextTransactionId - 1);
uint256 count = 0;
for (uint256 i = 1; i < nextTransactionId; i++) {
if (!transactions[i].executed) {
pending[count] = i;
count++;
}
}
// Resize array to actual count
uint256[] memory result = new uint256[](count);
for (uint256 i = 0; i < count; i++) {
result[i] = pending[i];
}
return result;
}
/**
* @notice Get all signer addresses
* @return Array of the 5 signer addresses
*/
function getSigners() external view returns (address[5] memory) {
return signers;
}
/**
* @notice Check if a contract is authorized to use this Multi-Sig
* @param contractAddress The contract address to check
* @return True if authorized, false otherwise
*/
function isAuthorizedContract(address contractAddress) external view returns (bool) {
return _isAuthorizedContract(contractAddress);
}
}"
},
"contracts/utils/Registry.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./MultiSig.sol";
/**
* @title Registry
* @dev The bean registry contract for mapping contract names to addresses and tracking registration status.
*/
contract Registry is Ownable(msg.sender) {
/// @notice Maps contract names to their addresses.
mapping(string => address) public registry;
/// @notice Tracks whether an address is registered.
mapping(address => bool) public registered;
// =============================================================
// EVENTS
// =============================================================
/**
* @notice Emitted when a contract address is set in the registry.
* @param name The name of the contract.
* @param contractAddress The address of the contract.
* @param setter The address that set the registry entry.
*/
event RegistryAddressSet(
string indexed name,
address indexed contractAddress,
address indexed setter
);
/**
* @notice Emitted when a transaction is delegated to Multi-Sig
* @param txId The transaction ID from Multi-Sig
* @param caller The address that initiated the call
* @param functionName The function being called
*/
event TransactionDelegated(
uint256 indexed txId,
address indexed caller,
string functionName
);
// =============================================================
// WRITE FUNCTIONS
// =============================================================
/**
* @notice Gets the MultiSig contract instance from registry
* @return The MultiSig contract instance, or address(0) if not set
*/
function _getMultiSig() internal view returns (MultiSig) {
address multiSigAddress = registry["MultiSig"];
if (multiSigAddress == address(0)) {
return MultiSig(address(0));
}
return MultiSig(multiSigAddress);
}
/**
* @notice Sets the contract address for a given name.
* @dev Delegates to Multi-Sig if called by user, executes directly if called by Multi-Sig.
* @param _name The name of the contract.
* @param _address The address associated with the contract.
*/
function setContractAddress(
string memory _name,
address _address
) external {
MultiSig multiSig = _getMultiSig();
if (address(multiSig) != address(0) && msg.sender != address(multiSig)) {
// User called this function - delegate to Multi-Sig
bytes memory data = abi.encodeWithSignature("setContractAddress(string,address)", _name, _address);
string memory description = string(abi.encodePacked("Set contract address: ", _name));
uint256 txId = multiSig.delegateTransaction(address(this), data, description);
emit TransactionDelegated(txId, msg.sender, "setContractAddress");
return;
}
if (address(multiSig) == address(0)) {
require(msg.sender == owner(), "Registry: Only owner can call when Multi-Sig not set");
} else {
require(msg.sender == address(multiSig), "Registry: Only Multi-Sig can call this function");
}
require(_address != address(0), "Registry: Cannot set zero address");
require(bytes(_name).length > 0, "Registry: Contract name cannot be empty");
address existingAddress = registry[_name];
if (existingAddress != address(0) && existingAddress != _address) {
registered[existingAddress] = false;
}
registry[_name] = _address;
registered[_address] = true;
emit RegistryAddressSet(_name, _address, msg.sender);
}
/**
* @notice Toggles the registration status of an address.
* @dev Delegates to Multi-Sig if called by user, executes directly if called by Multi-Sig.
* @param _address The address to toggle registration status.
*/
function toggleRegistration(address _address) external {
MultiSig multiSig = _getMultiSig();
if (address(multiSig) != address(0) && msg.sender != address(multiSig)) {
bytes memory data = abi.encodeWithSignature("toggleRegistration(address)", _address);
string memory description = string(abi.encodePacked("Toggle registration for: ", _addressToString(_address)));
uint256 txId = multiSig.delegateTransaction(address(this), data, description);
emit TransactionDelegated(txId, msg.sender, "toggleRegistration");
return;
}
if (address(multiSig) == address(0)) {
require(msg.sender == owner(), "Registry: Only owner can call when Multi-Sig not set");
} else {
require(msg.sender == address(multiSig), "Registry: Only Multi-Sig can call this function");
}
require(_address != address(0), "Registry: Cannot toggle registration for zero address");
registered[_address] = !registered[_address];
}
// =============================================================
// READ FUNCTIONS
// =============================================================
/**
* @notice Retrieves the contract address associated with a given name.
* @param _name The name of the contract.
* @return The address associated with the provided name.
*/
function getContractAddress(
string memory _name
) external view returns (address) {
require(
registry[_name] != address(0),
string(abi.encodePacked("Registry: Does not exist ", _name))
);
return registry[_name];
}
/**
* @notice Checks if a given address is registered.
* @param _address The address to check.
* @return True if the address is registered, false otherwise.
*/
function isRegistered(address _address) external view returns (bool) {
return registered[_address];
}
/**
* @notice Get the Multi-Sig contract address
* @return The address of the Multi-Sig contract
*/
function getMultiSig() external view returns (address) {
return registry["MultiSig"];
}
// =============================================================
// UTILITY FUNCTIONS
// =============================================================
/**
* @dev Convert address to string for descriptions
*/
function _addressToString(address addr) internal pure returns (string memory) {
bytes32 value = bytes32(uint256(uint160(addr)));
bytes memory alphabet = "0123456789abcdef";
bytes memory str = new bytes(42);
str[0] = '0';
str[1] = 'x';
for (uint256 i = 0; i < 20; i++) {
str[2+i*2] = alphabet[uint8(value[i + 12] >> 4)];
str[3+i*2] = alphabet[uint8(value[i + 12] & 0x0f)];
}
return string(str);
}
}"
}
},
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"evmVersion": "paris",
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
}
}
}}
Submitted on: 2025-09-26 11:32:52
Comments
Log in to comment.
No comments yet.