[USCF 시리즈 (3/4)] 어떻게 스마트 컨트랙트를 업그레이드 가능하도록 바꿀 수 있는가?

How can we make Smart Contract Upgradeable?

Youngtaek (Robbie) OH
HAECHI AUDIT
14 min readDec 11, 2018

--

블록체인 기술 전문 기업 ‘해치랩스(HAECHI LABS)’에서 업그레이드 가능한 스마트 컨트랙트 프레임워크에 대한 글을 연재합니다.

  1. 왜 우리는 업그레이드 가능한 스마트 컨트랙트가 필요한가?
  2. 업그레이드 가능한 스마트 컨트랙트를 위한 필요 조건과 배경지식
  3. 어떻게 스마트 컨트랙트를 업그레이드 가능하도록 바꿀 수 있을까?
  4. vvisp을 통해 DApp 업그레이드하기

시리즈 2편에서는 크게 두 가지 내용을 알아보았습니다. 우선 업그레이드 가능한 스마트 컨트랙트는 다음과 같은 조건을 만족시켜야 함을 알게 됐습니다.

  • 컨트랙트를 업그레이드할 시에 주소가 바뀌지 않아야 사용자가 이용하기에 편리합니다.
  • 업그레이드를 하더라도 기존 컨트랙트에 저장되어 있던 정보가 사라지지 않아야 합니다.
  • 여러 개의 컨트랙트를 업그레이드하는 중에 서로 버전이 다른 시점이 없어야 합니다.

또한 위의 조건을 만족하는 업그레이드 가능한 스마트 컨트랙트를 구현하기 위해서는, Ethereum Virtual Machine(EVM)의 delegatecall과 storage라는 개념을 이해하여야 하므로 이 부분에 대해서도 간략히 알아보았습니다. 이 부분도 간략히 요약하자면 아래와 같습니다.

  • delegatecall을 이용하면 호출 컨트랙트가 타겟 컨트랙트의 함수를 이용할 수 있습니다.
  • delegatecall로 타겟 컨트랙트의 함수를 불러도 실행 결과는 호출 컨트랙트에 저장됩니다.
  • storage에는 slot이 있으며 각 변수들은 선언된 순서에 따라 서로 다른 slot에 저장됩니다.

이번 시리즈에서 다루게 될 내용은 크게 다음과 같습니다. 우선 그동안 컨트랙트를 업그레이드 가능하게 만들기 위해 어떠한 전략들이 있었는지 설명을 드리겠습니다. 그 다음, HAECHI LABS는 그 중 어떠한 전략을 바탕으로 더 나은 Upgradable Smart Contract Framework을 만들었는지 소개하도록 하겠습니다.

Existing Strategies

기존에도 Zeppelin 등 다양한 팀에서 업그레이드 가능한 스마트 컨트랙트를 연구해왔습니다. 기존에 연구되던 내용은 크게 두 가지 방식으로 나누어 볼 수 있습니다.

데이터와 로직의 분리 패턴

[그림 1]

이 방법은 상태를 저장하는 별도의 컨트랙트를 데이터베이스처럼 배포하여 사용하는 전략입니다. 이 때에 실제 로직이 구현되어있는 Business 컨트랙트와 데이터만을 저장히기 위한 Eternal Storage 컨트랙트로 나뉩니다. Eternal Storage는 아래와 같이 자료형별 key-value 저장소 형태로 구현합니다.

[그림 2]

Business 컨트랙트에서는 다음과 같이 개발자가 정한 key 생성 규칙에 따라 해시함수로 key를 만든 후, Eternal Storage에 있는 값들을 참조하거나 변경합니다.

[그림 3]

만약 이 상황에서 기존의 Business 컨트랙트를 새로운 버전으로 업그레이드 한다면 아래와 같이 이전 버전과 같은 Eternal Storage를 바라보는 새로운 Business 컨트랙트를 배포합니다.

[그림 4]

이러한 방식의 장점은 우선 새로운 버전의 컨트랙트를 사용하여도 기존에 저장되었던 storage를 그대로 사용할 수 있다는 것입니다. 하지만 위와 같은 업그레이드 방식은 몇 가지 단점을 가지고 있습니다. 우선 사용자는 매번 새로 배포되는 Business 컨트랙트 주소를 알아야 합니다. 이럴 경우 매 업데이트마다 모든 사용자에게 새로운 컨트랙트 주소를 전파해야 하는 번거로움이 있습니다. 이외에도 또 다른 불편함이 있습니다. 위에서 설명하였다시피 Eternal Storage로부터 데이터를 가져오기 위해서는 key 생성 규칙을 미리 정해야 하고, 그 규칙에 따라 매번 key를 만든 후 storage의 함수를 호출하여야 합니다. 이런 방법은 기존에 컨트랙트 내 변수를 참조할 때보다 직관적이지 못할 뿐만 아니라 경우에 따라서 이 규칙을 잘 파악하지 못한 또 다른 작업자가 업데이트 할 때에 실수를 할 가능성이 많습니다.

따라서 이러한 방식은 시리즈 2편에서 다뤘던 업그레이드 가능한 스마트 컨트랙트가 갖춰야 할 조건 중에 한 가지 조건만을 만족합니다.

  • 컨트랙트를 업그레이드할 시에 주소가 바뀌지 않아야 사용자가 이용하기에 편리합니다. (X)
  • 업그레이드를 하더라도 기존 컨트랙트에 저장되어 있던 정보가 사라지지 않아야 합니다. (O)
  • 여러 개의 컨트랙트를 업그레이드하는 중에 서로 버전이 다른 시점이 없어야 합니다. (X)

Proxy(프록시) 패턴

[그림 5]

Proxy 패턴은 사용자가 Business 컨트랙트 대신 Proxy 컨트랙트를 바라보도록 설계된 업그레이드 패턴입니다. 사용자는 컨트랙트 함수를 Proxy를 통해 호출하면, Proxy는 Business 컨트랙트 안에 있는 함수를 delegatecall하게 됩니다.

[그림 6]

또한 업그레이드를 할 시에는 Proxy 컨트랙트는 그대로 둔 채로 Business 컨트랙트만 새로 배포한 후, Proxy 컨트랙트가 새로 배포한 컨트랙트를 바라보도록 설정해줍니다.

[그림 7]

이런 방법은 다음과 같은 장점이 있습니다. 우선 저번 시리즈에서 설명하였다시피 delegatecall을 통해 함수를 호출하므로 실제로 storage는 Business 컨트랙트가 아닌 Proxy 컨트랙트에 저장됩니다. 따라서 Business 컨트랙트를 변경하여도 기존에 저장된 storage는 그대로 쓸 수 있습니다. 또한 사용자 입장에서 업그레이드하여도 Proxy가 변경되지 않기 때문에 주소가 바뀌지 않습니다. 여기에 더하여 Proxy 패턴은 “데이터와 로직의 분리 패턴”과 다르게 기존 컨트랙트에서 변수를 참조하는 법과 동일하게 작성하여도 되기 때문에 개발하기 간편하다는 이점이 있습니다.

따라서 이러한 방식은 시리즈 2편에서 다뤘던 업그레이드 가능한 스마트 컨트랙트가 갖춰야 할 조건 중에 두 가지 조건을 만족합니다.

  • 컨트랙트를 업그레이드할 시에 주소가 바뀌지 않아야 사용자가 이용하기에 편리합니다. (O)
  • 업그레이드를 하더라도 기존 컨트랙트에 저장되어 있던 정보가 사라지지 않아야 합니다. (O)
  • 여러 개의 컨트랙트를 업그레이드하는 중에 서로 버전이 다른 시점이 없어야 합니다. (X)

하지만 위의 두 방법으로도 여전히 풀지 못한 숙제가 있습니다. 이들은 모두 Business 컨트랙트가 하나만 존재할 경우만을 고려하고 있습니다. 따라서 여러 개의 컨트랙트를 업그레이드하는 도중에 서로 버전이 다른 시점이 없어야 한다는 조건은 고려되고 있지 않았습니다.

HAECHI LABS’ Upgradeable Smart Contract Framework

HAECHI LABS는 위에서 말한 두 가지 전략을 참고하여, 앞서 언급한 모든 조건들을 만족하는 업그레이드 가능한 스마트 컨트랙트 아키텍쳐를 설계하였습니다. 이것을 Upgradeable Smart Contract Framework(이하 USCF)라고 부릅니다.

Overall Structure

HAECHI LABS는 크게 Registry, Proxy, Business 라고 하는 3개의 레이어로 USCF 구조를 설계하였습니다. Proxy와 Business 레이어에 대해서는 앞서 설명 드렸습니다. 새로 추가된 레이어는 Registry입니다.

[그림 8]

Registry 컨트랙트의 upgradeToAndCalls라는 함수를 통하여 여러 컨트랙트를 동시에 atomic하게 업그레이드할 수 있습니다. Registry 컨트랙트에는 모든 Proxy 컨트랙트들의 주소가 저장되어 있으며, 각 Proxy 컨트랙트에는 현재 버전의 Business 컨트랙트가 등록이 되어 있습니다. Business 컨트랙트들에는 실제 서비스에 활용되는 로직이 구현되어 있습니다.

Registry 컨트랙트의 필요성

Registry 컨트랙트는 한 서비스를 구성하고 있는 전체 컨트랙트들을 업그레이드 하는 기능을 가지고 있습니다.

특히 주목할 부분은 Registry 컨트랙트의 upgradeToAndCalls라는 함수입니다. 하나의 서비스는 대체로 여러 개의 스마트 컨트랙트로 구성됩니다. 따라서 서비스의 특정 부분을 업데이트하려면 하나 이상의 스마트 컨트랙트들을 동시에 업그레이드해야 합니다. 이 때에 방법에 따라 업그레이드 중에 서로 버전이 다른 컨트랙트가 존재할 수 있습니다. 이는 앞서 설명 하였던 업그레이드 가능한 컨트랙트가 가져야할 세 번째 조건을 만족하지 못합니다.. 즉, 한 서비스에서 버전은 atomic하게 변경되어야 합니다.

아래 그림 9는 atomic하게 변경되지 않은 예시입니다. 그림에서 ContractA의 버전은 2이지만, ContractB의 버전은 1입니다. 이렇게 컨트랙트 간 버전이 서로 다르면 여러 가지 문제가 발생할 수 있습니다. 예를 들어 ContractA에서 버전 2부터 지원이 되는 ContractB의 함수를 부를 수 없습니다.

[그림 9]

Registry 컨트랙트의 upgradeToAndCalls 함수는 바로 이런 문제를 해결할 수 있도록 설계됐습니다. 스마트 컨트랙트를 호출한 하나의 트랜잭션 안에서 여러 개의 트랜잭션이 추가적으로 발생하게 되면, 이들을 “Internal Transaction”이라 부르며 이에 해당하는 트랜잭션들은 전부 atomic하게 처리됩니다.

[그림 10]

그림 10를 보면, Registry 컨트랙트의 upgradeToAndCalls이라는 함수 내에서 여러 개의 컨트랙트를 업그레이드 하는 로직이 있습니다. 이 로직이 실행되면서 이미 등록된 Proxy 컨트랙트를 업그레이드하는 트랜잭션들이 내부적으로 발생하기 때문에 이 트랜잭션들은 Internal 트랜잭션으로 처리되어 블록체인에 동시에 반영됩니다. 결과적으로 업그레이드하는 중에 각 컨트랙트들의 버전은 항상 같을 수 밖에 없습니다.

Proxy 컨트랙트의 작동 원리

Proxy 컨트랙트의 기능과 장점은 앞서 Proxy 패턴을 소개할 때에 설명하였습니다. 여기서는 Proxy 컨트랙트의 구체적인 동작 방법에 대해 설명해보겠습니다.

사용자는 Business 컨트랙트의 함수 methodA를 실행하고 싶을 때에 직접 Business 컨트랙트를 호출하지 않고, Proxy를 통해 호출합니다. 다시 말하면 사용자는 Proxy의 함수 methodA를 호출하라고 요청을 합니다. 하지만 Proxy에는 methodA가 구현되어있지 않습니다. 보통 이런 경우 에러가 발생하게 됩니다. 하지만 Proxy 컨트렉트에는 곧 설명드릴 fallback 함수의 성질 때문에 에러가 나지 않고 정상적으로 Business 컨트랙트의 함수를 이용할 수 있습니다.

fallback 함수는 유저가 컨트랙트에 선언되어 있지 않은 함수를 호출하였을 때 실행되는 특별한 함수입니다. Proxy에는 실제 비즈니스 로직이 구현되어 있지 않으므로 사용자가 호출했을 때에는 무조건 fallback 함수가 실행됩니다.

[그림 11]

fallback 함수의 구현은 어셈블리로 되어있는데 이 코드의 의미는 간략히 요약하면 다음과 같습니다. 우선 함수 call과 관련된 데이터를 복사합니다. 그런 다음 Business 컨트랙트에 복사한 호출 정보를 delegatecall을 이용하여 함수를 실행합니다. 마지막으로 실행한 결과를 복사하여 사용자에게 최종적으로 넘겨줍니다. 만약 실행 결과가 올바르지 않았다면 revert를 냅니다. 이렇게 구현하면 Proxy 컨트랙트에 함수의 실행 결과가 쌓이게 됩니다. 따라서 업그레이드 하여도 주소가 바뀌지 않으면서 데이터를 그대로 유지할 수 있습니다.

Business 컨트랙트 작성시 유의사항

Business 컨트랙트에는 서비스에 필요한 각종 함수와 변수를 정의합니다. 만약 특정 함수를 변경해야 하거나 추가해야 할 경우가 생긴다면 다음과 같은 사항을 주의하며 컨트랙트를 새로 짠 후 배포하면 됩니다.

  1. 이전 버전의 Business 컨트랙트를 상속한 후 구현을 시작합니다.
  2. Business 컨트랙트에서는 constructor를 쓰면 안됩니다. constructor에 해당하는 함수를 별도로 구현합니다(해당 함수는 initialize라는 이름을 추천합니다).
  3. 변수는 struct 등을 쓰지 말고 flat하게 선언합니다.

1)를 지켜야 하는 이유는 이전 버전에 선언되고 쓰여졌던 변수를 덮어쓰지 않아야 하기 때문입니다. 이더리움 storage의 구조상 컨트랙트에 생성된 변수는 정의된 순서대로 slot 번호를 부여받습니다. 만약 상속을 받지 않고 완전히 새로 컨트랙트를 구현한다면, 다시 slot 번호가 처음부터 부여되기 때문에 이전 버전의 변수를 덮어쓰게 됩니다.

2)를 지켜야 하는 이유는 실제로 컨트랙트 변수가 저장되는 곳이 Proxy 컨트랙트이어야 하기 때문입니다. 이를 위해서는 Business 컨트랙트의 함수는 항상 Proxy가 호출할 수 있어야 하는데, constructor는 컨트랙트가 배포될 때만 실행되는 특별한 함수이기 때문에 Proxy가 delegatecall로 실행시킬 수 없습니다. 따라서 constructor의 실행 결과는 Proxy가 아닌 Business 컨트랙트에 저장될 수밖에 없으므로, initialize와 같은 이름의 별도 함수로 이를 구현해야 합니다.

3)을 지켜야 하는 이유도 storage와 관련되어 있습니다. flat하게 펼치면 slot 번호가 순서대로 부여되기 때문에 상속받기만 한다면 추후에 변수를 추가하더라도 slot 번호가 중복되는 일이 없어 storage에서 충돌이 나지 않습니다. 하지만 struct 등을 사용하여 변수를 선언하게 된다면 업그레이드로 인해 struct 내부의 field를 추가했을 때, 기존에다른 변수가 사용하는 storage 영역이 struct에 새로 추가된 field와 충돌되어 덮어씌워질 수 있습니다.

위와 같은 유의사항을 지킨 후 Business 컨트랙트를 작성하여 배포합니다. 그 다음 Registry 컨트랙트를 이용해 업그레이드를 수행하면 안전하게 수정된 버전의 컨트랙트를 사용할 수 있습니다.

Conclusion

이번 편은 주로 Zeppelin 등의 회사가 기존에 연구하던 방법, HAECHI LABS가 만든 Upgradeable Smart Contract Framework의 구조에 대해 알아보았습니다. 저희의 USCF 구조를 간략히 요약하면 다음과 같습니다.

  • Registry 컨트랙트는 다수의 Business 컨트랙트의 atomic한 업그레이드를 위해 설계됐습니다.
  • Proxy 컨트랙트는 유저에게 업그레이드 시에도 동일한 주소와 동일한 storage 정보를 보장하기 위해 설계됐습니다.
  • Business 컨트랙트는 실제 서비스에 필요한 로직이 구현되어 있습니다. 몇 가지 유의사항을 지킨다면 일반적인 컨트랙트 개발과 동일하게 개발한 후 배포하면 됩니다.

하지만 위의 배경지식을 제대로 파악하고 Registry 컨트랙트부터 하나씩 직접 배포하는 과정은 매우 번거로운 일입니다. 그래서 HAECHI LABS는 업그레이드 가능한 스마트 컨트랙트를 쉽게 배포하기 위한 Command Line Interface를 개발하여 오픈소스(vvisp)로 공개하였습니다. 다음 포스팅에서는 해당 도구를 이용하여 어떻게 USCF를 쉽고 빠르게 배포하는지, 그리고 해당 도구가 어떻게 만들어 졌는지 설명드리도록 하겠습니다.

--

--

Youngtaek (Robbie) OH
HAECHI AUDIT

Decipher, Haechi Labs, Computer Science(graduate student), B.S in Astronomy