Parity Multisig Wallet 동결과 EIP999

Jason Kim
HAECHI AUDIT
Published in
17 min readMay 12, 2018
출처: https://www.parity.io/

2017년 11월 6일 02:33:47 PM +UTC, Parity사의 Multisig Wallet Library 스마트 컨트랙트에서 발견된 보안적 결함으로 인해 587개의 지갑과 그 안에 담겨있던 513,774.16 ETH 가 출금이 불가능하게 동결되었다. 이로 인해 Parity Multisig Wallet 을 사용해서 ICO 자금을 보관하던 Polkadot 과 같은 회사들의 막대한 자금이 동결됬다. 5개월이 지난 2018년 4월, 이 자금들을 복구하기위해 Parity 사의 “Afri Schoedon”가 EIP(Ethereum Improvement Proposals) 999 를 제출하였다. 그리고 현재 “Fellowship of Ethereum Magicians” 에서 이 EIP999를 어떻게 처리할지에 대해 활발하게 논의되고 있다. 어떻게 하다가 한 순간에 51만 ETH 가 그림의 떡이 되어버린 걸까? 이번글에서는 Parity Multisig Wallet 동결 사건이 어떻게 발생했는지, 스마트 컨트랙트에 어떤 보안적 취약점이 있었는지 살펴보고, EIP999 에서 제시하는 해결책에 대해 알아보고자 한다.

(들어가기에 앞서) Multisig Wallet 이란 ?

Parity Multisig Wallet 에 대해 살펴보기전에 Multisig Wallet 에 대해 간단하게 살펴보려고 한다. Multisig Wallet 은 트랜잭션 실행전에 여러 사람들에게 트랜잭션 실행에 대한 동의 여부를 받고 미리 설정한 최소 동의수를 넘으면 트랜잭션을 실행시키는 것이다. ICO 같이 큰 규모의 자금 조달을 한 뒤에 회사나 재단의 설립인들이 Multisig Wallet 에 자금을 넣어두면 한 명이 모든 권한을 가지고 자금을 관리하는 것이 아니라 여러명의 동의를 통해 자금을 관리할 수 있게 된다. 이 뿐만 아니라 거래소 지갑같은 경우에도 Multisig Wallet 을 사용하는 경우가 있다. 예를들어 트랜잭션이 발생하기위해 3개의 키중 2개의 키가 필요하다면 거래소가 1개, 보안업체가 1개, 개인이 1개를 가지고 사용하는 경우가 있다. 만약에 개인의 부주의로 해커가 키를 탈취하더라도 나머지 2개의 키중 1개가 더 필요하기 때문에 마음대로 트랜잭션을 발생시킬 수 없다. 이더리움 상에서 Multisig Wallet 의 구현은 스마트 컨트랙트를 통해 구현이 되며 대표적인 예로는 Gnosis 의 Multisig Wallet 과 Parity 의 Multisig Wallet 이 있다.

1. Parity Multisig Wallet 는 어떤 구조로 짜여졌을까?

Parity Multisig Wallet 에는 크게 2개의 스마트 컨트랙트가 존재한다. 첫번째는 각각의 지갑에 해당하는 Wallet 스마트 컨트랙트이고, 다른 하나는 WalletLibrary 스마트 컨트랙트로 지갑들이 가지고 있는 공통된 기능을 모듈화 시킨 스마트 컨트랙트이다. Parity Multisig Wallet 은 WalletLibrary 스마트 컨트랙트에 delegateCall 을 함으로써 동작하는 기능들이 많이 존재한다. Library 스마트 컨트랙트가 무엇인지 delegateCall 이 무엇인지 생소할 수 있기 때문에 Parity Multisig Wallet 구조를 살펴보기전에 간단하게 Library 와 delegateCall 이 무엇인지 살펴보자.

1–1. Library 스마트 컨트랙트란?

Solidity 프로그램을 만들 때 대다수가 Zeppelin 에서 만든 “openzeppelin-solidity” 오픈소스를 이용해서 만들 것이다. “openzeppelin-solidity”는 이미 검증되고 자주 사용하는 기능들을 모듈화시킨 오픈소스로 Integer Overflow 를 막아주는 “SafeMath” 같은 코드들을 포함한다. 우리가 토큰 세일 스마트 컨트랙트를 만들 때 이러한 오픈소스를 사용한다면 사용한 오픈소스 코드까지 토큰 세일 스마트 컨트랙트 코드에 포함해서 배포해야한다. 하지만 이렇게 오픈소스를 복사해서 배포하는 방식은 크게 3가지 문제점을 가지고 있다고 Zeppelin사에서 언급하고 있다. 첫번째로 트랜잭션에 코드를 담을 때 오픈소스까지 포함되야되서 배포할 때 사용되는 가스의 양이 많아진다. 두번째로 블록체인 상에 같은 코드의 반복이 많아진다. 이는 블록체인에 쓸데없는 데이터들이 떠다니는 것과 같다. 마지막으로 버그 수정이나 업데이트가 있을 때 각각의 프로젝트에서 배포를 다시 해줘야한다. 다시말하자면 “SafeMath” 오픈소스에 문제가 있거나 새로운 기능이 추가되면 “SafeMath” 를 사용하는 모든 프로젝트가 새롭게 스마트 컨트랙트를 배포 해야된다는 것이다. 이러한 문제점을 해결하기 위해서 나온 개념이 Library 스마트 컨트랙트이다. 자주 사용되는 스마트 컨트랙트 코드를 한번만 배포하고 이를 이용하는 스마트 컨트랙트들은 delegateCall 을 이용해서 Library 스마트 컨트랙트의 함수를 호출하는 것이다.

출처: 솔리디티 공식 문서

위의 Library 스마트 컨트랙트의 예제를 살펴보자. 3번째 줄을 살펴보면 Set이라는 스마트 컨트랙트의 키워드가 contract 가 아니라 library 임을 알 수 있다. Library 스마트 컨트랙트를 작성할 때는 contract 가 아니라 library 키워드를 사용할 수 있고, Set 의 함수를 호출 할 때는 50번째 줄처럼 Set.insert(..) 를 통해서 할 수 있다. 50번째 줄을 보면 delegateCall 을 직접 사용하고 있지 않지만 컴파일 타임에 컴파일러가 bytecode 로 변환할 때 delegateCall 로 변경해준다. library 키워드를 안쓰고 contract 키워드를 사용해서 Library 용도의 스마트 컨트랙트를 배포하면 어떨까? 다시말하자면 3번째 줄을 작성할 때 contract Set 을 사용하고 50번째 줄에서 setAddress.delegateCall(...) 이런식으로 작성하는 것이다. 이럴 경우에도 사실 똑같은 역할을 하게된다. library 키워드를 이용하면 이더리움을 받을 수 없고 state 변수들을 사용할 수 없다는 제약사항들이 있는데 contract 키워드를 이용해서 Library 용도의 스마트 컨트랙트를 만들면 이러한 제약사항들로 부터 자유롭다. Parity Multisig Wallet Library 스마트 컨트랙트가 Library 용으로 스마트 컨트랙트를 만들긴 했지만 library 키워드를 쓴 것이 아니라 contract 키워드를 사용했고 함수 호출을 위해서 delegateCall 을 사용했다.

1–2. DelegateCall 이란?

앞서 Library 스마트 컨트랙트가 왜 존재하는지 살펴보았다. 이번에는 Library 스마트 컨트랙트의 함수를 호출하기 위해 delegateCall 를 사용하는데 이것이 무언인지 살펴보자. 솔리디티 공식문서에 따르면 delegateCall 은 message call 의 특별한 형태이며, 함수 실행은 delegateCall 을 하는 컨트랙트의 컨텍스트에서 이뤄지고 msg.value , msg.sender 는 변경되지않는다. 마찬가지로 storage, balance등도 delegateCall 을 하는 스마트 컨트랙트의 정보를 참조하고 코드 실행만 delegateCall 로 호출당하는 스마트 컨트랙트의 것을 사용한다.

앞의 개념 설명만으로는 이해가 잘되지 않기 때문에 간단한 예제를 통해서 delegateCall 에 대해 살펴보자. 위의 스마트 컨트랙트를 보면 CallerContractCalleeContract 두 개가 있고 CallerContract 에서는 delegateCallCalleeContractcallCalleeContract 2개의 함수가 존재한다. 둘다 똑같이 CalleeContractcallMe 함수를 호출하지만 하나는 일반적인 call 을 이용하고 하나는 delegateCall 을 이용한다. Remix 를 이용해서 위의 코드를 돌려보면 calldelegateCall 의 차이를 명확히 알 수 있다.

CalleeContract's 주소: 0xbcaafb4802c27917d29449c8ab707ff6f86193ef

CallerContract's 주소: 0x2b47f0d926a28adfc90a69939502dcb687ac3686

함수호출자의 주소: 0xca35b7d915458ef540ade6068dfe2f44e8fa733c

함수호출 시 전달한 이더리움의 양: 1 ETH

callCalleeContract 의 결과
delegateCallCalleeContract 의 결과

각 함수를 호출했을 때 Event 로그를 살펴보자. call 을 이용해서 호출했을 시에는 msg.senderCallerContract 의 주소이다. 반면에 delegateCall 을 이용했을 시에는 함수를 호출한 주소이다. msg.value 를 살펴보면 함수 호출자가 전달한 1 ETH 가 delegateCall 을 했을 때는 전달되지만 call 을 호출하면 전달되지 않는 것을 알 수 있다. 그리고 파라미터로 전달한 n 값은 delegateCall 을 이용해서 호출하면 CallerContract 의 storage 에 저장된다. 반면 call 을 이용해서 호출하면 CalleeContract 의 storage 에 저장된다. 이를 통해 delegateCall 을 이용하면 호출하는 스마트 컨트랙트의 컨텍스트를 그대로 이용해서 호출당하는 함수의 코드를 실행시킬 수 있음을 알 수 있다.

1–3. Parity Multisig Wallet 구조

Parity Multisig Wallet 스마트 컨트랙트 코드를 보기위해 Library 스마트 컨트랙트와 delegateCall 에 대해 살펴봤다. 이번에는 문제가 되었던 스마트 컨트랙트의 코드를 살펴보고 어떻게 이뤄져있는지 알아보자. Parity Multisig Wallet 스마트 컨트랙트는 크게 2개의 스마트 컨트랙트로 이뤄져있다. 첫번째는 WalletLibrary 스마트 컨트랙트로 Multisig Wallet 에서 제공하는 입출금같은 핵심기능들의 구현체이다. 다음으로 Wallet 스마트 컨트랙트로 Parity 클라이언트를 이용해서 지갑을 생성하면 지갑마다 Wallet 스마트 컨트랙트가 배포된다. 이번 지갑 동결에서 가장 많은 이더리움이 잠긴 Polkadot의 지갑Wallet 스마트 컨트랙트이다. Wallet 스마트 컨트랙트는 지갑 사용자가 Parity 클라이언트를 이용해서 지갑에서 이더리움을 전송하는 트랜잭션을 발생시키면 WalletLibrary 스마트 컨트랙트의 함수를 delegateCall 한다.

위는 Wallet 스마트 컨트랙트의 fallback function 이다. 여기를 보면 msg.value 가 있는 경우(지갑에 이더리움을 입금하는경우)를 제외한 모든 경우 delegateCall 을 통해서 WalletLibrary 스마트 컨트랙트에 동작을 위임한다. 그래서 WalletLibrary 에 버그가 있을 경우 모든 Wallet 스마트 컨트랙트는 입금외에는 동작할 수 없게되는 것이다.

위의 코드는 WalletLibrary 스마트 컨트랙트에 구현된 함수들이나 internal 함수들은 제외시켰다. 해당 함수들은 하루에 출금할 수 있는 이더리움의 양, 지갑 주인을 몇명으로 설정할지, 몇명의 주인에게 출금 동의를 받아야 출금이 되는지에 관련된 함수들이다. 인터페이스말고 실제 구현 코드는 etherscan 에서 볼 수 있다.

2. Parity Multisig Wallet 는 어떻게 동결됬는가?

드디어 Parity Multisig Wallet 이 동결되는 과정을 살펴볼 시간이다. 어떤 과정을 통해 지갑들이 동결되었는지 시간순으로 살펴보려고 한다.

  1. Parity Multisig Wallet 1차 해킹 사건: 지갑 동결사건 전에도 Parity Multisig Wallet 에는 문제가 있었다. 이 문제를 해결하기 위해 기존의 버그를 수정한다.
  2. WalletLibrary 배포: Jul-20–2017 04:39:46 PM +UTC 에 0x863df6bfa4469f3ead0be8f9f2aae51c91a907b4 주소를 가지는 WalletLibrary 를 배포한다.
  3. 다양한 Parity Multisig Wallet 의 생성: Polkadot 을 포함해 다양한 회사, 재단, 팀들이 지갑을 생성하였다.
  4. WalletLibraryinitWallet호출: Nov-06–2017 02:33:47 PM +UTC 에
    0xae7168deb525862f4fee37d987a971b385b96952 주소에서 initWallet 을 호출하였다. WalletLibraryinitWallet 은 새롭게 지갑이 생성되면 Wallet 스마트 컨트랙트를 배포하고, delegateCall 을 통해 Wallet 을 초기화하기 위한 기능이다. 하지만 initWalletWallet 스마트 컨트랙트의 delegateCall 을 통해서 호출하는것 뿐만아니라 직접 호출할 수 도 있다. 이러한점을 간과하고 Parity 에서는 WalletLibrary 를 배포하고 initWallet 을 호출하지 않았다. 따라서 누구나 해당 함수를 호출해서 WalletLibrary 의 주인이 될 수 있는 상태였다
  5. WalletLibrarykill 호출: Nov-06–2017 03:25:21 PM +UTC 에 앞에서 initWallet 을 호출했던 주소에서 kill 함수를 호출하게 된다. initWallet 을 통해 WalletLibrary 의 주인이 되었기 때문에 주인만 호출할 수 있는 함수인 kill 함수를 호출할 수 있게된다. kill 함수는 suicide를 호출한다. suicide 는 지금은 deprecated 되어서 selfdestruct 으로 대체되었다. selfdstruct는 블록체인에서 스마트 컨트랙트 코드를 제거하고 남아있는 이더리움을 지정된 주소로 보내는 함수이다. suicide 역시 블록체인에서 스마트 컨트랙트 코드를 지우는 것이다. 즉, 많은 Wallet 스마트 컨트랙트가 delegateCall 을 통해 호출하던 WalletLibrary 스마트 컨트랙트가 사라진 것이다.
  6. 익명의 개발자의 github 이슈제기: WalletLibrarykill 한 개발자가 등장해서 Parity 의 github 에 이슈를 제기한다. 그리고 Pairty 개발자들은 문제를 파악하기 시작한다.

위와 같은 일련의 과정을 통해 2017년 7월 20일에 WalletLibrary 가 배포된 이후에 생성된 Multisig Wallet 들이 동결되었다. 물론 해당 사건 이후 문제가 수정된 Multisig Wallet 스마트 컨트랙트가 나와서 지금은 문제가 없다. 하지만 사소한 실수로인해 막대한 양의 이더리움이 동결되었고 현재(2018년 5월 12일)에도 잠긴 이더리움을 찾을 수 는 없다. 만약에 Wallet 스마트 컨트랙트에서 delegateCall 하는 WalletLibrary 의 주소를 수정할 수 있었다면 해결책이 있었을 수 있다. 하지만 Wallet 스마트 컨트랙트에서 WalletLibrary 는 2017년 7월 20일에 배포된 컨트랙트 주소로 하드코딩 되어있기 때문에 수정이 불가능하다.

3. 동결 사건을 해결하기 위한 EIP999 란?

2018년 4월 4일Parity 사의 Afri Schoedon 가 EIP999WalletLibrary 의 복구를 요청한다. 기존에 제안된 논의들은 EVM에서 변경이 필요했는데 이번에 제안된 EIP 에서는 단일 상태 변경을 통해서 동결된 지갑에 접근할 수 있게 하자는 내용이다. 이러한 제안이 나오는 이유는 이더리움에서는 self-destructed 스마트 컨트랙트에 대한 복구 기능이 없기 때문이다.

Parity 에서는 기존의 문제있던 코드들을 수정하였고 리뷰를 위해0x21C9E434c669c4d73f55215A6F2130A185E127AC 에 새로운 WalletLibrary 스마트 컨트랙트를 배포하였다. EIP999 에서 말하기를 새롭게 수정된 WalletLibrary 에는 크게 2가지 수정사항이 있다고 한다. 첫번째는 kill 함수의 제거이다. 기존에는 WalletLibrary 의 주인이 되면 kill 을 통해서 스마트 컨트랙트를 블록체인에서 삭제할 수 있었다. 하지만 이러한 기능자체를 만들어 두는 것이 위험하다고 생각한다. 만약에 스마트 컨트랙트 주인이 악의를 품고 kill을 호출한다면 또 다시 지갑 동결사태가 일어날 수 있기 때문이다. 따라서 이러한 문제점을 인식한 Parity 는 해당 함수를 삭제하였다. 다음으로는 WalletLibrary 생성시 주인을 0x0 으로 초기화 시키는 것이다. 지갑 동결 사태에서 Parity 가 했던 가장 큰 실수는 WalletLibrary 를 배포하고서 초기화를 시키지 않았던 것이다. 이러한 부분은 WalletLibrary 의 생성자에서 초기화 함수를 호출하는 것으로 해결할 수 있다. 아마도 스마트 컨트랙트를 리뷰했던 사람들은 당연히 Parity 가 새롭게 스마트 컨트랙트를 배포하고 초기화 할 것이라고 생각해서 생성자에 초기화 함수를 호출하는 것을 강제하지 않았던거 같다. 그리고 WalletLibrary주인을 0x0 으로 초기화 함으로써 아무도 해당 스마트 컨트랙트의 권한을 가지지 못하게 만들었다.

EIP999 에서는 위처럼 올바르게 수정된 코드를 suicideWalletLibrary주소에 배포하자는 제안이다. 이더리움 로드맵의 3단계에 해당하는 “메트로폴리스" 로 넘어가기 위해서 이더리움 재단은 2 단계로 나눠서 일을 진행하고 있는데 그 중 두번째 업데이트인 “콘스탄티노플"에 해당 EIP999 를 포함시키자는 것이다. EIP999 가 포함된다면 Parity Multisig Wallet 을 사용하는 사람들이 다시 동결되어있는 이더리움을 출금할 수 있게 된다. 하지만 커뮤니티에서는 블록체인의 불변성과 거리가 먼 행동이고 모든 버그들을 이런방식으로 해결하는것을 옳지 않다고 목소리를 내고 있다. 2018년 4월 17일 부터 4월 24일 까지 진행한 커뮤니티 투표에서도 EIP999 를 포함시키는 것에 반대한다는 의견이 55% 로 찬성측보다 많은 상황이다.

출처: etherchain.org

4. 결론 — 스마트 컨트랙트 Audit 의 중요성

Parity Multisig Wallet 동결 사건은 The DAO 해킹 사건처럼 이더리움이 해커의 손에 직접들어가는 해킹사건은 아니다. 그렇기 때문에 EIP999 를 통과시켜도 Parity Multisig Wallet 에 동결된 이더리움의 주인들이 아닌 다른 사람들은 이득을 보는 것이 없다. 기존에 The DAO 해킹 사건에서는 상당한 양의 이더리움이 해커의 손에 들어갔고 이로 인해 이더리움 생태계에 큰 영향을 미칠 수 있었기에 하드포크를 진행하게 됬던 것이다. 하지만 지갑 동결사건은 이더리움이 소각된것과 같은 효과를 가지기 때문에 생태계에 큰 위험이 되지 않는다. 오히려 Parity Multisig Wallet 의 버그를 수정하기 위한 업데이트를 할 때 하드포크가 일어날 수 있다는 위험성이 존재한다. 지갑이 동결 되었을 당시 Parity Multisig Wallet 스마트 컨트랙트가 Audit 을 받았냐는 질문이 많았는데 Parity 측에서는 이더리움 재단을 포함한 여러 개발자들에게 리뷰를 받았다고 한다. 전문가들 집단이여도 이처럼 사소한 실수로 인해 엄청난 경제적 피해를 입기때문에 스마트 컨트랙트에 익숙하지 않은 팀들에게 Audit 은 상당히 중요한 과정이다.

--

--

Jason Kim
HAECHI AUDIT

Haechi Labs CEO, Haechi Labs provides Henesis & Haechi Audit.