// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; /// originally from gnosis and just a simple update to 0.8 version /// @title Multisignature wallet - Allows multiple parties to agree on transactions before execution. /// @author Stefan George - <stefan.george@consensys.net> contract MultiSigWallet { /* * Events */ // 컨펌이 일어날때 event Confirmation(address indexed sender, uint indexed transactionId); // 컨펌이 일어났다가 취소가 될때 event Revocation(address indexed sender, uint indexed transactionId); // 트랜잭션을 등록(제안)하였을때 event Submission(uint indexed transactionId); // 컨펌이 완료되어 트랜잭션을 실행할때 event Execution(uint indexed transactionId); // 컨펌이 부족하여 트랜잭션 실행을 하지 않을때 event ExecutionFailure(uint indexed transactionId); // 코인을 입금할때 event Deposit(address indexed sender, uint value); // 오를 더할때 event OwnerAddition(address indexed owner); // 오를 제거할때 event OwnerRemoval(address indexed owner); // Requirement가 바뀔때 event RequirementChange(uint required); /* * Constants */ uint public constant MAX_OWNER_COUNT = 50; /* * Storage */ mapping(uint => Transaction) public transactions; mapping(uint => mapping(address => bool)) public confirmations; mapping(address => bool) public isOwner; address[] public owners; uint public required; // 컨펌 허용 수 uint public transactionCount; // 트랜잭션 인덱스 struct Transaction { address destination; uint value; bytes data; bool executed; } /* * Modifiers */ // 컨트랙트 내의 한 함수가 다른 함수를 직접 호출할 때 msg.sender == address(this) 조건이 성립됨 modifier onlyWallet() { require( msg.sender == address(this), "MultisigWallet: address is not a multisig wallet" ); _; } modifier ownerDoesNotExist(address owner) { require( !isOwner[owner], "MultisigWallet: address exists in owner addresses list" ); _; } modifier ownerExists(address owner) { require( isOwner[owner], "MultisigWallet: address not exists in owner addresses list" ); _; } modifier transactionExists(uint transactionId) { require( transactions[transactionId].destination != address(0), "MultisigWallet: transaction is not exist" ); _; } modifier confirmed(uint transactionId, address owner) { require( confirmations[transactionId][owner], "MultisigWallet: transaction is not confirmed" ); _; } modifier notConfirmed(uint transactionId, address owner) { require( !confirmations[transactionId][owner], "MultisigWallet: transaction is confirmed" ); _; } modifier notExecuted(uint transactionId) { require( !transactions[transactionId].executed, "MultisigWallet: transaction is executed before" ); _; } modifier notNull(address _address) { require(_address != address(0), "MultisigWallet: address is null"); _; } modifier validRequirement(uint ownerCount, uint _required) { require( ownerCount <= MAX_OWNER_COUNT && _required <= ownerCount && _required != 0 && ownerCount != 0, "MultisigWallet: owner requirement is not valid" ); _; } /// @dev Fallback function allows to deposit ether. receive() external payable { if (msg.value > 0) emit Deposit(msg.sender, msg.value); } /// @dev Contract constructor sets initial owners and required number of confirmations. /// @param _owners List of initial owners. /// @param _required Number of required confirmations. constructor( address[] memory _owners, uint _required ) validRequirement(_owners.length, _required) { for (uint i = 0; i < _owners.length; i++) { require(!isOwner[_owners[i]] && _owners[i] != address(0)); isOwner[_owners[i]] = true; } owners = _owners; required = _required; } /* * Public functions */ /// @dev Allows to add a new owner. Transaction has to be sent by wallet. /// @param owner Address of new owner. function addOwner( address owner ) public onlyWallet ownerDoesNotExist(owner) notNull(owner) validRequirement(owners.length + 1, required) { isOwner[owner] = true; owners.push(owner); emit OwnerAddition(owner); } /// @dev Allows to remove an owner. Transaction has to be sent by wallet. /// @param owner Address of owner. function removeOwner(address owner) public onlyWallet ownerExists(owner) { isOwner[owner] = false; for (uint256 i = 0; i < owners.length; i++) if (owners[i] == owner) { owners[i] = owners[owners.length - 1]; owners.pop(); break; } if (required > owners.length) changeRequirement(owners.length); emit OwnerRemoval(owner); } /// @dev Allows to replace an owner with a new owner. Transaction has to be sent by wallet. /// @param owner Address of owner to be replaced. /// @param newOwner Address of new owner. function replaceOwner( address owner, address newOwner ) public onlyWallet ownerExists(owner) ownerDoesNotExist(newOwner) { for (uint i = 0; i < owners.length; i++) if (owners[i] == owner) { owners[i] = newOwner; break; } isOwner[owner] = false; isOwner[newOwner] = true; emit OwnerRemoval(owner); emit OwnerAddition(newOwner); } /// @dev Allows to change the number of required confirmations. Transaction has to be sent by wallet. /// @param _required Number of required confirmations. function changeRequirement( uint _required ) public onlyWallet validRequirement(owners.length, _required) { required = _required; emit RequirementChange(_required); } /// @dev Allows an owner to submit and confirm a transaction. /// @param destination Transaction target address. /// @param value Transaction ether value. /// @param data Transaction data payload. function submitTransaction( address destination, uint value, bytes memory data ) public returns (uint transactionId) { transactionId = addTransaction(destination, value, data); confirmTransaction(transactionId); } /// @dev Allows an owner to confirm a transaction. /// @param transactionId Transaction ID. function confirmTransaction( uint transactionId ) public ownerExists(msg.sender) transactionExists(transactionId) notConfirmed(transactionId, msg.sender) { confirmations[transactionId][msg.sender] = true; emit Confirmation(msg.sender, transactionId); executeTransaction(transactionId); } /// @dev Allows an owner to revoke a confirmation for a transaction. /// @param transactionId Transaction ID. function revokeConfirmation( uint transactionId ) public ownerExists(msg.sender) confirmed(transactionId, msg.sender) notExecuted(transactionId) { confirmations[transactionId][msg.sender] = false; emit Revocation(msg.sender, transactionId); } /// @dev Allows anyone to execute a confirmed transaction. /// @param transactionId Transaction ID. function executeTransaction( uint transactionId ) public ownerExists(msg.sender) confirmed(transactionId, msg.sender) notExecuted(transactionId) { if (isConfirmed(transactionId)) { Transaction storage txn = transactions[transactionId]; txn.executed = true; if ( external_call( txn.destination, txn.value, txn.data.length, txn.data ) ) emit Execution(transactionId); else { emit ExecutionFailure(transactionId); txn.executed = false; } } } // call has been separated into its own function in order to take advantage // of the Solidity's code generator to produce a loop that copies tx.data into memory. function external_call( address destination, uint value, uint dataLength, bytes memory data ) internal returns (bool) { bool result; assembly { let x := mload(0x40) // "Allocate" memory for output (0x40 is where "free memory" pointer is stored by convention) let d := add(data, 32) // First 32 bytes are the padded length of data, so exclude that result := call( sub(gas(), 34710), // 34710 is the value that solidity is currently emitting // It includes callGas (700) + callVeryLow (3, to pay for SUB) + callValueTransferGas (9000) + // callNewAccountGas (25000, in case the destination address does not exist and needs creating) destination, value, d, dataLength, // Size of the input (in bytes) - this is what fixes the padding problem x, 0 // Output is ignored, therefore the output size is zero ) } return result; } /// @dev Returns the confirmation status of a transaction. /// @param transactionId Transaction ID. /// @return Confirmation status. function isConfirmed(uint transactionId) public view returns (bool) { uint count = 0; for (uint i = 0; i < owners.length; i++) { if (confirmations[transactionId][owners[i]]) count += 1; if (count == required) return true; } return false; } /* * Internal functions */ /// @dev Adds a new transaction to the transaction mapping, if transaction does not exist yet. /// @param destination Transaction target address. /// @param value Transaction ether value. /// @param data Transaction data payload. function addTransaction( address destination, uint value, bytes memory data ) internal notNull(destination) returns (uint transactionId) { transactionId = transactionCount; transactions[transactionId] = Transaction({ destination: destination, value: value, data: data, executed: false }); transactionCount += 1; emit Submission(transactionId); } /* * Web3 call functions */ /// @dev Returns number of confirmations of a transaction. /// @param transactionId Transaction ID. function getConfirmationCount( uint transactionId ) public view returns (uint count) { for (uint i = 0; i < owners.length; i++) if (confirmations[transactionId][owners[i]]) count += 1; } /// @dev Returns total number of transactions after filers are applied. /// @param pending Include pending transactions. /// @param executed Include executed transactions. function getTransactionCount( bool pending, bool executed ) public view returns (uint count) { for (uint i = 0; i < transactionCount; i++) if ( (pending && !transactions[i].executed) || (executed && transactions[i].executed) ) count += 1; } /// @dev Returns list of owners. /// @return List of owner addresses. function getOwners() public view returns (address[] memory) { return owners; } /// @dev Returns array with owner addresses, which confirmed transaction. /// @param transactionId Transaction ID. function getConfirmations( uint transactionId ) public view returns (address[] memory _confirmations) { address[] memory confirmationsTemp = new address[](owners.length); uint count = 0; uint i; for (i = 0; i < owners.length; i++) if (confirmations[transactionId][owners[i]]) { confirmationsTemp[count] = owners[i]; count += 1; } _confirmations = new address[](count); for (i = 0; i < count; i++) _confirmations[i] = confirmationsTemp[i]; } /// @dev Returns list of transaction IDs in defined range. /// @param from Index start position of transaction array. /// @param to Index end position of transaction array. /// @param pending Include pending transactions. /// @param executed Include executed transactions. function getTransactionIds( uint from, uint to, bool pending, bool executed ) public view returns (uint[] memory _transactionIds) { uint[] memory transactionIdsTemp = new uint[](transactionCount); uint count = 0; uint i; for (i = 0; i < transactionCount; i++) if ( (pending && !transactions[i].executed) || (executed && transactions[i].executed) ) { transactionIdsTemp[count] = i; count += 1; } _transactionIds = new uint[](to - from); for (i = from; i < to; i++) _transactionIds[i - from] = transactionIdsTemp[i]; } }