본문 바로가기

분석/블록체인

[Ethereum] Multisig에 대해 알아보자

Multisig

개념

보안을 강화하기 위해 여러 명의 서명을 통해 트랜잭션을 실행시키는 것을 mutisig 서명이라고 한다.

 

Gnosis Contract

Multisig Wallet

transactions는 transactionId마다 구조체로 이루어진 transaction 정보를 저장한다.

confirmations는 transactionId마다 owner들의 서명 유무를 저장한다.

owners는 onwers들을 저장한다.

required는 트랜잭션이 실행되기 위한 최소 서명 개수를 나타낸다.

uint public constant MAX_OWNER_COUNT = 50;

struct Transaction {
    address destination;
    uint value;
    bytes data;
    bool executed;
}
mapping(uint => Transaction) public transactions;

mapping(uint => mapping(address => bool)) public confirmations;

address[] public owners;

uint public required;

uint public transactionCount;

 

아래는 트랜잭션을 제안하는 로직으로 최초 관리자가 호출한다.

1씩 증가하는 정수로 transactionId를 만들고 transactions에 transaction 정보를 저장한다. 

function submitTransaction(
    address destination,
    uint value,
    bytes memory data
) public returns (uint transactionId) {
    transactionId = addTransaction(destination, value, data);
    confirmTransaction(transactionId);
}

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

 

아래는 트랜잭션을 confirm하는 로직으로 관리자들이 서명할때마다 호출된다.

confirmations에 해당 트랜잭션 아이디에 호출자가 서명했다는 정보를 저장한다.

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

 

아래는 트랜잭션을 실행시키는 로직으로 서명 개수가 required에 도달하면 트랜잭션을 실행시킨다.

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

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

 

테스트

아래는 multisig를 통해 owner3으로 ERC-20 토큰을 100개 전송하는 트랜잭션을 실행하는 것을 검증하는 로직이다.

1. ERC-20 토큰 100개를 전송하는 트랜잭션을 owner1로 서명을 한다.

    pending 상태인 트랜잭션이 1개인지 검증한다.

2. pendig 상태인 트랜잭션 아이디를 조회하고 해당 아이디가 0인지 검증한다.

3. 아이디가 0번인 트랜잭션을 owner2로 서명을 하고

    Confirmation 이벤트, Execution 이벤트 , Transfer 이벤트가 발생했는지 검증한다.

describe('Multisig test', async () => {
  let erc20Contract: Contract;
  let multisigContract: Contract;
  
  let owner1: SignerWithAddress;
  let owner2: SignerWithAddress;
  let owner3: SignerWithAddress;
  let transactionIds: number;
  let transferCalldata: string;
  const grantAmount = 100;

  it('set data', async () => {
    [owner1, owner2, owner3] = await ethers.getSigners();
  });

  describe('Test MultisigContract Deployment', () => {
    it('deploy', async () => {
      const ERC20 = await ethers.getContractFactory("MyERC20");
      erc20Contract = await ERC20.deploy();
      await erc20Contract.deployed();
        
      const owners = [
        owner1.address,
        owner2.address,
        owner3.address
      ]

      const Multisig = await ethers.getContractFactory("MultiSigWallet");
      multisigContract = await Multisig.deploy(owners,2);
      await multisigContract.deployed();
    });

    it('should check submitTransaction', async () => {
      const erc20Token = await ethers.getContractAt("MyERC20", erc20Contract.address);
      
      await erc20Contract.mint(multisigContract.address, changeToBigInt(grantAmount))
      transferCalldata = erc20Token.interface.encodeFunctionData("transfer", [owener3.address, changeToBigInt(grantAmount)]);

      await multisigContract.submitTransaction(
        erc20Contract.address,
        0,
        transferCalldata,
      )
      expect(toBigNumber(await multisig.getTransactionCount(true,false))).to.equal(toBigNumber(1))
    });

    it('should get transaction ids', async () => {  
      transactionIds = await multisig.getTransactionIds(0,1,true,false)
      expect(toBigNumber(transactionIds)).to.equal(toBigNumber(0))
    });

    it('should check confirmTransactions and check execution', async () => {
      expect(await multisigContract.connect(owner2).confirmTransaction(0))
      .to.emit(multisigContract, 'Confirmation')
      .withArgs(owner2.address, 0)
      .to.emit(multisigContract, 'Execution')
      .withArgs(0)
      .to.emit(erc20Contract, 'Transfer')
      .withArgs(multisigContract.address, ower3.address, changeToBigInt(grantAmount));
    });
  });

});