멀티 시그니쳐 월렛은 설정한 2개 이상의 주소가 owner 가 되어 각 주소가 signature 를 생성 후, tx 를 실행하는 방식이다.
예를 들어 2-3 멀티 시그니쳐 월렛의 경우 3개의 주소가 owner 가 되고, 2개 이상의 주소가 tx에 서명한다면 이 tx 를 실행하게 된다.
여러명이 owner 가 될 수도 있고, 한명이 여러개의 주소를 가지고 owner 가 될 수도 있다.
여러명이 owner 인 경우, 민주적인 방법으로 지갑을 제어할 수 있다. 하지만 그만큼 한번의 tx 실행 과정도 복잡하다.
한명이 여러개의 주소를 가지고 owner 가 된 경우, 지갑의 보안성을 높일 수 있다. 만약, 2-3 멀티 시그니쳐 월렛을 보유했을 경우 하나의 주소에 대한 제어를 잃게 되어도 여전히 2개 주소에 대한 제어가 가능하기에 멀티 시그니쳐 월렛의 제어 권한은 본인에게 있다.
글이 너무 길어지기에 코드와 주석으로만 정리한다..
// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
/*
Multi Signature Wallet
여러 주소가 Multi Signature Wallet 의 공동 owner 가 된다.
owner 들이 설정한 만큼 approve(confirm) 된 tx 는 실행이 가능하다.
- 테스트
TestContract 의 함수 callMe 호출하기
1. 주소 3개를 아래 그대로 복사해서 생성자의 주소로, numberOfConfirmRequired 는 2로 배포.
["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db"]
2. TestContract 배포. 원하는 값을 넣고 getData 함수 실행, bytes 값 반환
3. submit 함수 call. (TestContract 주소, value = 0, bytes = 위에 2번에서 얻은 bytes 값)
4. txId 0번을 tx를 지갑을 돌려가며 approve 한다.(1에서 설정한대로 2번 이상 approve)
5. txId 0번을 execution 한다. TestContract 의 i 값이 변경되었는지 확인.
*/
contract MultiSignatureWallet {
event Deposit(address indexed sender, uint value, uint balance);
event SubmitTransaction(
address indexed owner,
uint indexed txIndex,
address indexed to,
uint value,
bytes data
);
event Approve(address indexed owner, uint txId);
event Revoke(address indexed owner, uint txId);
event Execute(address indexed owner, uint txId);
struct Transaction {
address to;
uint value;
bytes data;
bool executed;
uint confirmation;
}
Transaction[] public transactions;
uint public numberOfConfirmRequired;
address[] public owners;
mapping(address => bool) isOwner;
mapping(uint => mapping(address => bool)) public isApproved;
// receive 로 받는 이더를 emit 처리
receive() external payable {
emit Deposit(msg.sender, msg.value, address(this).balance);
}
// owner 주소를 배열로 처리, 최소 approve 요구 값을 생성자로 설정
constructor(address[] memory _owners, uint _numberOfConfirmRequired) {
require(_numberOfConfirmRequired != 0
&& _numberOfConfirmRequired <= _owners.length,"Invaild confirm number.");
require(_owners.length > 1,"Minimum two addresses are required.");
for(uint i; i < _owners.length; i++){
require(_owners[i] != address(0),"Zero address.");
require(!isOwner[_owners[i]],"Invaild address");
owners.push(_owners[i]);
isOwner[_owners[i]] = true;
}
numberOfConfirmRequired = _numberOfConfirmRequired;
}
// 지정된 owner 인가?
modifier onlyOwner() {
require(isOwner[msg.sender], "Owner only.");
_;
}
// tx 가 실행되지 않았는가?
modifier checkExecute(uint _txId) {
require(!transactions[_txId].executed, "Already executed.");
_;
}
// msg.sender 가 특정 tx 를 approve 를 하지 않았는가?
modifier checkApprove(uint _txId) {
require(!isApproved[_txId][msg.sender], "Already approved.");
_;
}
// 존재하는 tx 인가?
modifier txExist(uint _txId) {
require(_txId <= transactions.length, "Already Executed.");
_;
}
// tx 제출. 다른 지갑의 approve 를 기다리게 됨.
function submit(address _to, uint _value, bytes memory _data) external onlyOwner {
require(_to != address(0), "Zero address.");
require(_value != 0 || _data.length != 0, "Invaild input.");
transactions.push(Transaction(_to, _value, _data, false, 0));
emit SubmitTransaction(msg.sender, transactions.length -1, _to, _value, _data);
}
// submit 된 tx Approve
function approve(uint _txId)
external
onlyOwner
checkApprove(_txId)
checkExecute(_txId)
txExist(_txId)
{
isApproved[_txId][msg.sender] = true;
transactions[_txId].confirmation += 1;
emit Approve(msg.sender, _txId);
}
// Approve 했던 tx 취소
function revoke(uint _txId)
external
onlyOwner
checkExecute(_txId)
txExist(_txId)
{
require(isApproved[_txId][msg.sender], "Not approved.");
isApproved[_txId][msg.sender] = false;
transactions[_txId].confirmation -= 1;
emit Revoke(msg.sender, _txId);
}
// gas tx execute ------ 가스비 비교(지역변수X)
// 86123 74889 53685
function execution(uint _txId)
external
onlyOwner
txExist(_txId)
{
// 지역변수 사용X, 가스비 소모가 가장 큼
require(!transactions[_txId].executed, "Already executed.");
require(numberOfConfirmRequired <= transactions[_txId].confirmation,"Not Approved.");
// re-entry 할 수 없도록 상태변수 변경 후 call
transactions[_txId].executed = true;
(bool success, ) = transactions[_txId].to.call
{value : transactions[_txId].value}(transactions[_txId].data);
require(success, "Failed to transact.");
emit Execute(msg.sender, _txId);
}
// gas tx execute ------ 가스비 비교(지역변수O storage)
// 85066 73970 52766
function execution_2(uint _txId)
external
onlyOwner
txExist(_txId)
{
// storage 사용 시 가스비가 제일 효율적임
Transaction storage transaction = transactions[_txId];
require(!transaction.executed, "Already executed.");
require(numberOfConfirmRequired <= transaction.confirmation,"Not Approved.");
// re-entry 할 수 없도록 상태변수 변경 후 call
transaction.executed = true;
(bool success, ) = transaction.to.call
{value : transaction.value}(transaction.data);
require(success, "Failed to transact.");
emit Execute(msg.sender, _txId);
}
// gas tx execute ------ 가스비 비교(지역변수O memory)
// 85980 74765 53561
function execution_3(uint _txId)
external
onlyOwner
txExist(_txId)
{
// memory 형태의 지역변수 사용으로는 상태변수 변경이 불가
Transaction memory transaction = transactions[_txId];
require(!transaction.executed, "Already executed.");
require(numberOfConfirmRequired <= transaction.confirmation,"Not Approved.");
// memory 를 사용하기에 이렇게 변경, 가스비도 많이 소모 됨
transactions[_txId].executed = true;
(bool success, ) = transaction.to.call
{value : transaction.value}(transaction.data);
require(success, "Failed to transact.");
emit Execute(msg.sender, _txId);
}
// 가장 최근의 txId 를 반환
function getTxCount() public view returns(uint){
return transactions.length;
}
}
// 테스트 컨트랙트
contract TestContract {
uint public i;
function callMe(uint j) public {
i += j;
}
// function Selector 역할. 원하는 함수와 인자를 묶어서 bytes 코드로 변환
function getData(uint _num) public pure returns (bytes memory) {
return abi.encodeWithSignature("callMe(uint256)", _num);
}
}
코드 내에 적어놓은 순서대로 테스트를 한다.
2
로 배포.["0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2","0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db"]