Gnosis Multisig Contract 뜯어먹기

프동프동·2023년 3월 27일
0

Blockchain Wallet

목록 보기
3/3
post-thumbnail

구성요소

  1. Owner
    • 조건을 설정하고 조건을 확인하는 기능
  2. Wallet
    • 지갑 자체의 기능

Flow

  1. 소유주 설정
    • 몇명의 소유자를 설정할 것인지 배열로 입력한다.
    • 몇명의 소유자 중 몇개의 Confirm을 받았을 때 트랜잭션을 보낼지에 대한 수 설정
  2. 소유자 중 한사람이 트랜잭션을 submit한다.
  3. submit한 소유자가 아닌 다른 소유주가 트랜잭션을 confirm한다.
    • 이때 조건으로 주어진 수만큼 confirm이 되면 트랜잭션이 실행된다.

시나리오

시나리오. 지갑 소유주는 3명이고 이중 2명이 허락해야 트랜잭션을 보낼 수 있다.

1. 소유주와 조건을 입력하여 배포한다.

  • MultiSigWallet() 배포 시 지갑 소유주들의 주소를 배열로 입력하고 조건을 함께 입력한다.
    • MultiSigWallet(_owners, _required)
      • _owners : ["0xdc1f5172A3F7b184126e228c26379EB65564f866","0x6225bDB7E5c1C76Da3ba5C409D314390b47597ca","0xd6A77e56f22F213CD0E3a7F86f2fb846E1d3EcF4"]
      • _required :
        • 2
  • 결과
    eth_sendTransaction
    
      Transaction: 0x0627d6e709b8d0b226c0dd93a2bc01d803d79f2b516c4e659823927bc2528979
      Contract created: 0x771c0fed5d0d203b34c644ff98ed7b084fd4db38
      Gas usage: 2056951
      Block Number: 1
      Block Time: Mon Mar 27 2023 10:27:04 GMT+0900 (대한민국 표준시)

2. 등록된 소유주 확인

  • getOwners() 실행
  • 결과
    0:
    address[]: 0xdc1f5172A3F7b184126e228c26379EB65564f866,0x6225bDB7E5c1C76Da3ba5C409D314390b47597ca,0xd6A77e56f22F213CD0E3a7F86f2fb846E1d3EcF4

3. MultiSigWallet에 ETH를 보낸다.

let multisigWalletAddress = '0x771C0fEd5d0D203b34C644Ff98ED7B084Fd4DB38'
eth.sendTransaction({from:"0xE664DA6CDaBC6163b9542B210A2bE1a6885730d3", to:multiSigWallet, value: 1000});
  1. multiSigWallet 컨트랙트 잔고 확인

    truffle(development)> eth.getBalance(multiSigWallet)
    '1000'

4. multiSigWallet에서 일반 지갑으로 이더 전송하기

1번째 소유주가 “0x2c51Ce6bfa3CF47A9a6D3a30c4CDeb0F79Bf4D7E” 주소로 500 wei를 보내려고 한다.

  • 3명의 소유주 중 2명의 소유주만 승인이 되면 트랜잭션을 보낼 수 있다.
    truffle(development)> eth.getBalance(multiSigWallet)
    '1000'
    1. 1번째 소유주인 유저가 SubmitTransaction()을 호출하여 트랜잭션을 생성한다.

      • 1번째 유저는 SubmitTransaction()을 호출함과 동시에 트랜잭션을 허락한 것이 된다.
      {
      	"address destination": "0x2c51Ce6bfa3CF47A9a6D3a30c4CDeb0F79Bf4D7E",
      	"uint256 value": "500",
      	"bytes data": "0x00"
      }
      • 제출된 트랜잭션을 보면 executed가 false인 것을 확인할 수 있다.
      0:
      address: destination 0x2c51Ce6bfa3CF47A9a6D3a30c4CDeb0F79Bf4D7E
      1:
      uint256: value 500
      2:
      bytes: data 0x00
      3:
      bool: executed false
    2. 2번째 소유주인 유저가 confirmTransaction()을 호출하여 1번째 유저가 생성한 트랜잭션을 허용한다.

    3. 결과 확인

      • MultiSig Wallet
        truffle(development)> eth.getBalance(multiSigWallet)
        '500'
      • ETH를 받은 주소 확인
        truffle(development)> eth.getBalance('0x2c51Ce6bfa3CF47A9a6D3a30c4CDeb0F79Bf4D7E');
        '100000000000000000500'

gnosis/MultiSigWallet 컨트랙트

https://github.com/gnosis/MultiSigWallet/blob/master/contracts/MultiSigWallet.sol

MultiSigWallet: 생성자 함수

function MultiSigWallet(address[] _owners, uint _required)
    public
    validRequirement(_owners.length, _required)
{
    for (uint i=0; i<_owners.length; i++) {
        require(!isOwner[_owners[i]] && _owners[i] != 0);
        isOwner[_owners[i]] = true;
    }
    owners = _owners;
    required = _required;
}
  • _owners: 지갑 소유자를 나타내는 주소 배열
  • _required: 트랜잭션 실행에 필요한 확인 횟수(최소 승인 또는 서명의 수)

addOwner

새 소유자를 지갑에 추가할 수 있는 함수

/// @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);
    OwnerAddition(owner);
}

Parameter

  • owner : 추가하려는 소유자의 주소

Body

  • 조건
    • ownerDoesNotExist : 추가하려는 소유주가 기존 소유자 목록에 이미 존재하는지 확인
    • notNull : 추가하려는 소유주의 주소가 null이 아닌지 확인
    • validRequirement : 새 소유주를 추가해도 허용되는 최대 소유자 수를 위반하지 않는지 확인
  • 모든 조건이 충족되면 isOwner 매핑이 업데이트되며 새 소유자를 포함하고 새 소유자 주소가 owners 배열 끝에 추가된다.
  • OwnerAddition 이벤트로 새 소유자가 추가됨을 알린다.

removeOwner

소유자가 소유자 목록에서 다른 소유자를 제거할 수 있도록 하는 함수

/// @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 (uint i=0; i<owners.length - 1; i++)
        if (owners[i] == owner) {
            owners[i] = owners[owners.length - 1];
            break;
        }
    owners.length -= 1;
    if (required > owners.length)
        changeRequirement(owners.length);
    OwnerRemoval(owner);
}

Parameter

  • owner : 제거할 소유주

Body

  • 조건
    • onlyWallet : 지갑 컨트랙트 자체에 의해서만 실행되기 위함
    • ownerExists : 입력받은 주소가 소유주 목록에 있는지 확인
  • isOwner[owner] = false를 통해 소유자가 더이상 지갑의 소유자가 아님을 반영
  • owners 배열을 돌며 제거할 소유자의 인덱스를 찾아 제거한다.
  • if (required > owners.length) : 소유자가 제거된 후 Wallet에 남아있는 소유자 수 보다 큰지 확인한다.
  • 위 조건이 충족되면 changeRequirement() 함수를 통해 트랜잭션 실행에 필요한 _required 횟수를 변경할 수 있다.

replaceOwner

지갑 소유주가 기존 소유자를 새 소유자로 교체할 수 있도록 하는 함수

기존 지갑 소유주만 실행할 수 있다.

/// @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;
    OwnerRemoval(owner);
    OwnerAddition(newOwner);
}

Parameter

  • owner : 대체할 소유자의 주소
  • newOwner : 새 소유자의 주소

Body

  • 조건
    • ownerDoesNotExist : 기존 지갑의 소유자가 아닌지 확인한다.
      • false일 경우 실행할 수 있다.
  • 제거된 소유자에 대한 OwnerRemoval 이벤트와 새 소유주에 대한 OwnerAddition 이벤트가 호출된다.

changeRequirement

트랜잭션 실행에 필요한 _required 횟수를 변경할 수 있다.

/// @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;
    RequirementChange(_required);
}

Parameter

  • _required : 변경하려는 조건의 수

Body

  • 조건
    • validRequirement : 변경하기전 유효한조건인지 확인한다.
      modifier validRequirement(uint ownerCount, uint _required) {
          require(ownerCount <= MAX_OWNER_COUNT
              && _required <= ownerCount
              && _required != 0
              && ownerCount != 0);
          _;
      }
      • ownerCount가 최대 소유자 수를 나타내는 값보다 작거나 같은지
      • _required로 받은 값이 ownerCount보다 작은지
      • _required가 0은 아닌지 확인한다.

위 모든것이 충족되면 새로운 조건을 매개변수로하여 RequirementChange 이벤트를 호출한다.

submitTransaction

소유자가 다른 소유자의 승인을 위해 새 트랜잭션을 생성하고 제출하는 함수

/// @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.
/// @return Returns transaction ID.
function submitTransaction(address destination, uint value, bytes data)
    public
    returns (uint transactionId)
{
    transactionId = addTransaction(destination, value, data);
    confirmTransaction(transactionId);
}

Parameter

  • destination : 목적지 주소
  • value : 전송될 ETH의 양
  • data : 트랜잭션에 포함될 페이로드

Return

  • transactionId : 생성된 트랜잭션의 ID

Body

  • 내부에서 addTransaction 함수를 호출하여 새 트랜잭션의 Id값을 가지고 confirmTransaction() 함수를 실행하여 트랜잭션을 확인한다.

confirmTransaction

소유자가 submitTransaction()을 사용하여 제출된 트랜잭션을 확인할 수 있도록 한다.

/// @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;
    Confirmation(msg.sender, transactionId);
    executeTransaction(transactionId);
}

Parameter

  • transactionId : 확인할 Transaction ID

Body

  • 조건
    • ownerExists : 호출한 주소가 지갑의 소유자인지 확인
    • transactionId : 트랜잭션의 ID를 이용해 해당 트랜잭션의 목적지 주소가 0인지 확인
    • transactionExists :해당 트랜잭션 ID를 가진 트랜잭션이 존재하는지 확인
  • 내용
    • 위의 조건을 모두 만족 시키면 트랜잭션 ID와 해당 함수를 호출한 주소와 함께 ‘확인’했다는 것을 confirmations 맵핑에 true로 기록한다.
    • Confirmation 이벤트로 확인됨을 알리고 executeTransaction() 함수를 호출하여 트랜잭션을 실행시킨다.
      • 이때 executeTransaction() 함수에서 조건에 맞는 소유자의 확인 횟수에 도달하면 트랜잭션이 실행되고 조건에 충족되지 않으면 다른 소유자가 확인하거나 거래가 취소될 때까지 보류 상태로 유지된다.

revokeConfirmation

multiSigWallet의 소유주가 특정 트랜잭션에 대한 확인을 취소할 수 있도록 하는 함수

소유주가 트랜잭션 확인은 했지만 마음이 바뀌면 해당 함수를 사용하여 취소하여 거래가 실행되지 않도록 할 수 있다.

/// @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;
    Revocation(msg.sender, transactionId);
}

Parameter

  • transactionId : 취소하려는 트랜잭션 ID

Body

  • 조건
    • ownerExists : 호출한 주소가 소유주 중 한명인지 확인
    • confirmed : confirmations 매핑에서 해당 트랜잭션에 소유주가 서명을 했는지 확인
    • notExecuted : 아직 트랜잭션이 실행되지 않았는지 확인
  • confirmations 매핑에서 해당 트랜잭션을 찾아 false로 변경한다.
  • Revocation 이벤트를 호출하여 취소됨을 알린다.

executeTransaction

확인된 트랜잭션을 실행하는 함수

/// @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))
            Execution(transactionId);
        else {
            ExecutionFailure(transactionId);
            txn.executed = false;
        }
    }
}

Parameter

  • transactionId : 실행시킬 트랜잭션 ID

Body

  • 조건
    • ownerExists : 호출한 주소가 지갑의 소유자인지 확인
    • transactionId : 트랜잭션의 ID를 이용해 해당 트랜잭션의 목적지 주소가 0인지 확인
    • transactionExists :해당 트랜잭션 ID를 가진 트랜잭션이 존재하는지 확인
  • 내용
    • isConfirmed() 함수를 호출하여 모든 조건이 충족되었는지 확인
    • 조건이 충족되면 트랜잭션 id를 통해 트랜잭션을 검색하고 해당 트랜잭션이 실행됨을 표시
    • external_call() 함수를 호출하여 트랜잭션 실행을 시도한다.
    • 실행되면 Execution() 이벤트를 호출
    • 실행되지 않으면 ExecutionFailure() 이벤트를 내보내고 트랜잭션의 실행 플래그를 false로 설정한다.

external_call

저수준 어셈블리 코드를 사용하여 대상 컨트랙트에서 함수를 호출하고 호출이 성공했는지 여부를 나타내는 부울 값을 반환하는 함수

external_call 함수는 executeTransaction 함수에서 필요한 소유자 수에 의해 확인된 트랜잭션을 실행하는 데 사용됩니다.

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

Parameter

  • destination : 함수 호출이 이루어져야하는 계약의 주소
  • value : 함수 호출 시 보낼 ETH의 양
  • dataLength : 함수 호출에 포함할 페이로드의 길이
  • data : 함수 호출에 포함할 데이터 페이로드

Return

함수 호출이 성공했는지 여부를 나타내는 부울 값을 반환한다.

Body

let x := mload(0x40) 
// 포인터 변수로 지정
let d := add(data, 32)
// 파라미터로 받은 data 32를 추가한다.
result := call(
            sub(gas, 34710),  
            destination,
            value,
            d,
            dataLength,       
            x,
            0               
)
  • call() : 외부 컨트랙트를 실행하는 저수준 함수, 7개의 매개변수를 사용한다.
    • gas : 호출에 사용할 가스의 양
    • destination : 호출할 컨트랙트의 주소
    • value : 호출과 함께 보낼 Ether의 양
    • inputOffset : 입력 데이터가 시작되는 메모리 오프셋
    • inputSize : 입력 데이터의 길이(바이트 단위)
    • outputOffset : 출력 데이터가 기록되어야 하는 메모리 오프셋
    • outputSize : 출력 데이터의 최대 길이

isConfirmed

주어진 거래가 필요한 수의 소유자에 의해 확인되었는지 확인하는 함수

/// @dev Returns the confirmation status of a transaction.
/// @param transactionId Transaction ID.
/// @return Confirmation status.
function isConfirmed(uint transactionId)
    public
    constant
    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;
    }
}

Parameter

  • transactionId : 확인할 트랜잭션 ID

Return

  • 필요한 확인 횟수에 도달하면 true 반환

Body

  • confirmations 매핑에서 트랜잭션을 찾아 각 소유자가 거래를 확인했는지 확인한다.
  • 필요한 확인 횟수에 도달하면 true를 반환한다.

addTransaction

트랜잭션을 생성하는 함수

/*
 * 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.
/// @return Returns transaction ID.
function addTransaction(address destination, uint value, bytes data)
    internal
    notNull(destination)
    returns (uint transactionId)
{
    transactionId = transactionCount;
    transactions[transactionId] = Transaction({
        destination: destination,
        value: value,
        data: data,
        executed: false
    });
    transactionCount += 1;
    Submission(transactionId);
}

Parameter

  • destination : 목적지 주소
  • value : 보내려는 eth 양
  • data : 트랜잭션에 포함될 페이로드

Retrun

  • transactionId : 생성된 트랜잭션의 id

Body

  1. Parameter로 받은 destination 주소가 null 값인지 체크
  2. 새 트랜잭션 ID 할당
  3. 할당된 트랜잭션 id와 전달받은 값으로 transactions 매핑에 추가
  4. 새 트랜잭션 ID로 Submission 이벤트 호출
  5. 새 transactionId 리턴

getConfirmationCount

특정 트랜잭션에 대한 충족된 확인의 수를 반환하는 함수

/*
 * Web3 call functions
 */
/// @dev Returns number of confirmations of a transaction.
/// @param transactionId Transaction ID.
/// @return Number of confirmations.
function getConfirmationCount(uint transactionId)
    public
    constant
    returns (uint count)
{
    for (uint i=0; i<owners.length; i++)
        if (confirmations[transactionId][owners[i]])
            count += 1;
}

Parameter

  • transactionId : 확인하고자 하는 트랜잭션 ID

Return

  • 트랜잭션에 충족된 확인 카운트

Body

owners에 등록된 만큼 반복문으로 반복하며 각 소유자에 대한 트랜잭션과 소유자에 대한 confirmations 매핑을 보고 해당 소유자가 트랜잭션을 확인했는지 확인하며 카운트를 증가시킨다.

getTransactionCount

지정된 필터를 적용하여 조건에 맞는 총 트랜잭션의 수를 반환하는 함수

/// @dev Returns total number of transactions after filers are applied.
/// @param pending Include pending transactions.
/// @param executed Include executed transactions.
/// @return Total number of transactions after filters are applied.
function getTransactionCount(bool pending, bool executed)
    public
    constant
    returns (uint count)
{
    for (uint i=0; i<transactionCount; i++)
        if (   pending && !transactions[i].executed
            || executed && transactions[i].executed)
            count += 1;
}

Parameter

  • pending : 트랜잭션의 pending 여부
  • executed : 트랜잭션의 executed 여부

Return

  • 위 조건에 맞는 트랜잭션의 수

Body

  • pending이 true면 아직 실행되지 않은 모든 트랜잭션
  • executed가 true면 이미 실행된 모든 트랜잭션
  • 모두 true이면 실행 상태에 관계 없이 모든 트랜잭션을 계산하여 조건에 맞는 총 트랜잭션의 수를 반환한다.

getOwners

지갑 소유주의 주소들을 확인할 수 있다.

/// @dev Returns list of owners.
/// @return List of owner addresses.
function getOwners()
    public
    constant
    returns (address[])
{
    return owners;
}

Return

  • 소유주들의 주소가 들어간 배열

getConfirmations

트랜잭션을 확인한 소유자 주소의 배열을 반환하는 함수

/// @dev Returns array with owner addresses, which confirmed transaction.
/// @param transactionId Transaction ID.
/// @return Returns array of owner addresses.
function getConfirmations(uint transactionId)
    public
    constant
    returns (address[] _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];
}

Parameter

  • transactionId : 확인하고자 하는 트랜잭션 ID

Return

  • 트랜잭션을 확인한 소유자 주소의 배열

Body

  • 확인된 주소를 저장할 임시 배열을 만든다.
  • 소유자 배열을 반복하며 각 소유자가 confirmations 매핑에 트랜잭션을 확인 했는지 확인한다.
  • 확인하였으면 임시 배열에 해당 소유자를 추가한다.
  • 확인된 주소와 동일한 길이의 배열에 복사 후 반환한다.

getTransactionIds

컨트랙트에 저장된 모든 트랜잭션 ID를 검색하는 데 사용된다.

/// @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.
/// @return Returns array of transaction IDs.
function getTransactionIds(uint from, uint to, bool pending, bool executed)
    public
    constant
    returns (uint[] _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];
}

Parameter

  • from : 보내는 주소
  • to : 받는 주소
  • pending : pending 여부
  • executed : executed 여부

Return

  • _transactionIds : 저장된 모든 트랜잭션 ID의 배열

Body

  • transactionCount의 길이와 동일한 임시 배열을 만든다.
  • pending, executed 조건에 맞는 트랜잭션을 찾아 transactionIdsTemp 배열에 추가한다.
  • from - to로 지정된 범위와 동일한 길이의 _transactionIds 배열을 생성한다.
  • _transactionIds 배열에 위 조건에서 찾은 transactionIdsTemp을 추가하여 반환한다.
profile
좋은 개발자가 되고싶은

0개의 댓글