본문으로 건너뛰기
Change page

스마트 컨트랙트 업그레이드

이더리움의 스마트 컨트랙트는 이더리움 가상 머신(EVM)에서 실행되는 자동 실행 프로그램입니다. 이 프로그램들은 설계상 불변이므로, 컨트랙트가 배포된 후에는 비즈니스 로직을 업데이트할 수 없습니다.

불변성은 스마트 컨트랙트의 무신뢰성, 탈중앙화 및 보안에 필수적이지만, 특정 경우에는 단점이 될 수 있습니다. 예를 들어, 불변 코드는 개발자가 취약한 컨트랙트를 수정하는 것을 불가능하게 만들 수 있습니다.

그러나 스마트 컨트랙트 개선에 대한 연구가 증가함에 따라 여러 업그레이드 패턴이 도입되었습니다. 이러한 업그레이드 패턴을 통해 개발자는 비즈니스 로직을 다른 컨트랙트에 배치하여 (불변성을 유지하면서) 스마트 컨트랙트를 업그레이드할 수 있습니다.

전제 조건

스마트 컨트랙트, 스마트 컨트랙트 구조이더리움 가상 머신(EVM)에 대해 잘 이해하고 있어야 합니다. 또한 이 가이드는 독자가 스마트 컨트랙트 프로그래밍에 대한 지식이 있다고 가정합니다.

스마트 컨트랙트 업그레이드란 무엇인가요?

스마트 컨트랙트 업그레이드는 컨트랙트의 상태를 유지하면서 스마트 컨트랙트의 비즈니스 로직을 변경하는 것을 포함합니다. 특히 스마트 컨트랙트의 맥락에서 업그레이드 가능성과 가변성이 동일하지 않다는 점을 명확히 하는 것이 중요합니다.

이더리움 네트워크의 주소에 배포된 프로그램은 여전히 변경할 수 없습니다. 하지만 사용자가 스마트 컨트랙트와 상호 작용할 때 실행되는 코드는 변경할 수 있습니다.

이는 다음 방법을 통해 수행할 수 있습니다.

  1. 스마트 컨트랙트의 여러 버전을 생성하고 이전 컨트랙트에서 컨트랙트의 새 인스턴스로 상태(즉, 데이터)를 마이그레이션합니다.

  2. 비즈니스 로직과 상태를 저장하기 위해 별도의 컨트랙트를 생성합니다.

  3. 프록시 패턴을 사용하여 불변 프록시 컨트랙트에서 수정 가능한 로직 컨트랙트로 함수 호출을 위임합니다.

  4. 특정 함수를 실행하기 위해 유연한 위성 컨트랙트와 인터페이스하고 이에 의존하는 불변 메인 컨트랙트를 생성합니다.

  5. 다이아몬드 패턴을 사용하여 프록시 컨트랙트에서 로직 컨트랙트로 함수 호출을 위임합니다.

업그레이드 메커니즘 #1: 컨트랙트 마이그레이션

컨트랙트 마이그레이션은 동일한 소프트웨어의 고유한 상태를 생성하고 관리한다는 개념인 버전 관리를 기반으로 합니다. 컨트랙트 마이그레이션은 기존 스마트 컨트랙트의 새 인스턴스를 배포하고 스토리지와 잔액을 새 컨트랙트로 전송하는 것을 포함합니다.

새로 배포된 컨트랙트는 빈 스토리지를 가지므로 이전 컨트랙트에서 데이터를 복구하여 새 구현에 쓸 수 있습니다. 그 후에는 이전 컨트랙트와 상호 작용했던 모든 컨트랙트를 업데이트하여 새 주소를 반영해야 합니다.

컨트랙트 마이그레이션의 마지막 단계는 사용자가 새 컨트랙트를 사용하도록 설득하는 것입니다. 새 컨트랙트 버전은 사용자 잔액과 주소를 유지하여 불변성을 보존합니다. 토큰 기반 컨트랙트인 경우 거래소에 연락하여 이전 컨트랙트를 폐기하고 새 컨트랙트를 사용하도록 해야 합니다.

컨트랙트 마이그레이션은 사용자 상호 작용을 중단하지 않고 스마트 컨트랙트를 업그레이드하기 위한 비교적 간단하고 안전한 조치입니다. 그러나 사용자 스토리지와 잔액을 새 컨트랙트로 수동으로 마이그레이션하는 것은 시간이 많이 걸리고 높은 가스 비용이 발생할 수 있습니다.

컨트랙트 마이그레이션에 대해 자세히 알아보기 (opens in a new tab)

업그레이드 메커니즘 #2: 데이터 분리

스마트 컨트랙트를 업그레이드하는 또 다른 방법은 비즈니스 로직과 데이터 스토리지를 별도의 컨트랙트로 분리하는 것입니다. 이는 사용자가 로직 컨트랙트와 상호 작용하는 동안 데이터는 스토리지 컨트랙트에 저장됨을 의미합니다.

로직 컨트랙트에는 사용자가 애플리케이션과 상호 작용할 때 실행되는 코드가 포함되어 있습니다. 또한 스토리지 컨트랙트의 주소를 보유하고 이와 상호 작용하여 데이터를 가져오고 설정합니다.

한편, 스토리지 컨트랙트는 사용자 잔액 및 주소와 같은 스마트 컨트랙트와 관련된 상태를 보유합니다. 스토리지 컨트랙트는 로직 컨트랙트가 소유하며 배포 시 후자의 주소로 구성된다는 점에 유의하세요. 이렇게 하면 승인되지 않은 컨트랙트가 스토리지 컨트랙트를 호출하거나 데이터를 업데이트하는 것을 방지할 수 있습니다.

기본적으로 스토리지 컨트랙트는 불변이지만, 가리키는 로직 컨트랙트를 새 구현으로 교체할 수 있습니다. 이렇게 하면 스토리지와 잔액을 그대로 유지하면서 EVM에서 실행되는 코드가 변경됩니다.

이 업그레이드 방법을 사용하려면 스토리지 컨트랙트에서 로직 컨트랙트의 주소를 업데이트해야 합니다. 앞서 설명한 이유로 새 로직 컨트랙트를 스토리지 컨트랙트의 주소로 구성해야 합니다.

데이터 분리 패턴은 컨트랙트 마이그레이션에 비해 구현하기가 더 쉽다고 할 수 있습니다. 그러나 악의적인 업그레이드로부터 스마트 컨트랙트를 보호하려면 여러 컨트랙트를 관리하고 복잡한 권한 부여 체계를 구현해야 합니다.

업그레이드 메커니즘 #3: 프록시 패턴

프록시 패턴은 또한 데이터 분리를 사용하여 비즈니스 로직과 데이터를 별도의 컨트랙트에 유지합니다. 그러나 프록시 패턴에서는 스토리지 컨트랙트(프록시라고 함)가 코드 실행 중에 로직 컨트랙트를 호출합니다. 이는 로직 컨트랙트가 스토리지 컨트랙트를 호출하는 데이터 분리 방법의 반대입니다.

프록시 패턴에서 일어나는 일은 다음과 같습니다.

  1. 사용자는 데이터를 저장하지만 비즈니스 로직을 보유하지 않는 프록시 컨트랙트와 상호 작용합니다.

  2. 프록시 컨트랙트는 로직 컨트랙트의 주소를 저장하고 delegatecall 함수를 사용하여 모든 함수 호출을 로직 컨트랙트(비즈니스 로직 보유)에 위임합니다.

  3. 호출이 로직 컨트랙트로 전달된 후 로직 컨트랙트에서 반환된 데이터를 검색하여 사용자에게 반환합니다.

프록시 패턴을 사용하려면 delegatecall 함수에 대한 이해가 필요합니다. 기본적으로 delegatecall는 컨트랙트가 다른 컨트랙트를 호출할 수 있도록 하는 연산 코드이며, 실제 코드 실행은 호출하는 컨트랙트의 컨텍스트에서 발생합니다. 프록시 패턴에서 delegatecall를 사용하는 것의 의미는 프록시 컨트랙트가 내부 함수를 호출하는 것처럼 스토리지에 읽고 쓰며 로직 컨트랙트에 저장된 로직을 실행한다는 것입니다.

Solidity 문서 (opens in a new tab) 발췌:

메시지 호출의 특별한 변형인 delegatecall이 존재합니다. 이는 대상 주소의 코드가 호출하는 컨트랙트의 컨텍스트(즉, 주소)에서 실행되고 msg.sendermsg.value가 값을 변경하지 않는다는 점을 제외하면 메시지 호출과 동일합니다. 이는 컨트랙트가 런타임에 다른 주소에서 코드를 동적으로 로드할 수 있음을 의미합니다. 스토리지, 현재 주소 및 잔액은 여전히 호출하는 컨트랙트를 참조하며 코드만 호출된 주소에서 가져옵니다.

프록시 컨트랙트에는 fallback 함수가 내장되어 있기 때문에 사용자가 함수를 호출할 때마다 delegatecall를 호출해야 한다는 것을 알고 있습니다. Solidity 프로그래밍에서 폴백 함수 (opens in a new tab)는 함수 호출이 컨트랙트에 지정된 함수와 일치하지 않을 때 실행됩니다.

프록시 패턴이 작동하도록 하려면 프록시 컨트랙트가 지원하지 않는 함수 호출을 처리하는 방법을 지정하는 사용자 지정 폴백 함수를 작성해야 합니다. 이 경우 프록시의 폴백 함수는 delegatecall을 시작하고 사용자의 요청을 현재 로직 컨트랙트 구현으로 다시 라우팅하도록 프로그래밍됩니다.

프록시 컨트랙트는 기본적으로 불변이지만 업데이트된 비즈니스 로직이 있는 새 로직 컨트랙트를 생성할 수 있습니다. 그런 다음 업그레이드를 수행하는 것은 프록시 컨트랙트에서 참조되는 로직 컨트랙트의 주소를 변경하는 문제입니다.

프록시 컨트랙트가 새 로직 컨트랙트를 가리키도록 하면 사용자가 프록시 컨트랙트 함수를 호출할 때 실행되는 코드가 변경됩니다. 이를 통해 사용자에게 새 컨트랙트와 상호 작용하도록 요청하지 않고도 컨트랙트의 로직을 업그레이드할 수 있습니다.

프록시 패턴은 컨트랙트 마이그레이션과 관련된 어려움을 제거하기 때문에 스마트 컨트랙트를 업그레이드하는 데 널리 사용되는 방법입니다. 그러나 프록시 패턴은 사용하기가 더 복잡하며 부적절하게 사용할 경우 함수 선택자 충돌(function selector clashes) (opens in a new tab)과 같은 치명적인 결함을 유발할 수 있습니다.

프록시 패턴에 대해 자세히 알아보기 (opens in a new tab).

업그레이드 메커니즘 #4: 전략 패턴

이 기술은 특정 기능을 구현하기 위해 다른 프로그램과 인터페이스하는 소프트웨어 프로그램을 생성하도록 권장하는 전략 패턴 (opens in a new tab)의 영향을 받았습니다. 이더리움 개발에 전략 패턴을 적용한다는 것은 다른 컨트랙트의 함수를 호출하는 스마트 컨트랙트를 구축하는 것을 의미합니다.

이 경우 메인 컨트랙트에는 핵심 비즈니스 로직이 포함되어 있지만 특정 함수를 실행하기 위해 다른 스마트 컨트랙트("위성 컨트랙트")와 인터페이스합니다. 이 메인 컨트랙트는 또한 각 위성 컨트랙트의 주소를 저장하고 위성 컨트랙트의 다른 구현 간에 전환할 수 있습니다.

새 위성 컨트랙트를 구축하고 새 주소로 메인 컨트랙트를 구성할 수 있습니다. 이를 통해 스마트 컨트랙트의 전략(즉, 새 로직 구현)을 변경할 수 있습니다.

앞서 논의한 프록시 패턴과 유사하지만, 사용자가 상호 작용하는 메인 컨트랙트가 비즈니스 로직을 보유한다는 점에서 전략 패턴은 다릅니다. 이 패턴을 사용하면 핵심 인프라에 영향을 주지 않고 스마트 컨트랙트에 제한적인 변경 사항을 도입할 수 있는 기회를 얻을 수 있습니다.

주요 단점은 이 패턴이 주로 사소한 업그레이드를 출시하는 데 유용하다는 것입니다. 또한 메인 컨트랙트가 손상된 경우(예: 해킹을 통해) 이 업그레이드 방법을 사용할 수 없습니다.

업그레이드 메커니즘 #5: 다이아몬드 패턴

다이아몬드 패턴은 프록시 패턴의 개선으로 간주될 수 있습니다. 다이아몬드 프록시 컨트랙트는 둘 이상의 로직 컨트랙트에 함수 호출을 위임할 수 있기 때문에 다이아몬드 패턴은 프록시 패턴과 다릅니다.

다이아몬드 패턴의 로직 컨트랙트는 패싯(facets)이라고 합니다. 다이아몬드 패턴이 작동하도록 하려면 프록시 컨트랙트에서 함수 선택자 (opens in a new tab)를 다른 패싯 주소에 매핑하는 매핑을 생성해야 합니다.

사용자가 함수 호출을 수행하면 프록시 컨트랙트는 매핑을 확인하여 해당 함수 실행을 담당하는 패싯을 찾습니다. 그런 다음 (폴백 함수를 사용하여) delegatecall를 호출하고 호출을 적절한 로직 컨트랙트로 리디렉션합니다.

다이아몬드 업그레이드 패턴은 기존 프록시 업그레이드 패턴에 비해 몇 가지 장점이 있습니다.

  1. 모든 코드를 변경하지 않고도 컨트랙트의 작은 부분을 업그레이드할 수 있습니다. 업그레이드에 프록시 패턴을 사용하려면 사소한 업그레이드라도 완전히 새로운 로직 컨트랙트를 생성해야 합니다.

  2. 모든 스마트 컨트랙트(프록시 패턴에 사용되는 로직 컨트랙트 포함)에는 24KB 크기 제한이 있으며, 이는 특히 더 많은 함수가 필요한 복잡한 컨트랙트의 경우 제한이 될 수 있습니다. 다이아몬드 패턴은 여러 로직 컨트랙트에 함수를 분할하여 이 문제를 쉽게 해결할 수 있도록 합니다.

  3. 프록시 패턴은 접근 제어에 포괄적인(catch-all) 접근 방식을 채택합니다. 업그레이드 함수에 접근할 수 있는 엔티티는 컨트랙트 전체를 변경할 수 있습니다. 그러나 다이아몬드 패턴은 모듈식 권한 접근 방식을 가능하게 하여 엔티티가 스마트 컨트랙트 내의 특정 함수만 업그레이드하도록 제한할 수 있습니다.

다이아몬드 패턴에 대해 자세히 알아보기 (opens in a new tab).

스마트 컨트랙트 업그레이드의 장단점

장점단점
스마트 컨트랙트 업그레이드를 통해 배포 후 단계에서 발견된 취약점을 더 쉽게 수정할 수 있습니다.스마트 컨트랙트를 업그레이드하면 코드 불변성이라는 개념이 무효화되며, 이는 탈중앙화 및 보안에 영향을 미칩니다.
개발자는 로직 업그레이드를 사용하여 탈중앙화 애플리케이션(dapp)에 새로운 기능을 추가할 수 있습니다.사용자는 개발자가 스마트 컨트랙트를 임의로 수정하지 않을 것이라고 신뢰해야 합니다.
버그를 신속하게 수정할 수 있으므로 스마트 컨트랙트 업그레이드는 최종 사용자의 안전을 향상시킬 수 있습니다.스마트 컨트랙트에 업그레이드 기능을 프로그래밍하면 복잡성이 한층 더해지고 치명적인 결함의 가능성이 높아집니다.
컨트랙트 업그레이드는 개발자에게 다양한 기능을 실험하고 시간이 지남에 따라 디앱을 개선할 수 있는 더 많은 여지를 제공합니다.스마트 컨트랙트를 업그레이드할 수 있는 기회는 개발자가 개발 단계에서 실사를 수행하지 않고 프로젝트를 더 빨리 시작하도록 부추길 수 있습니다.
스마트 컨트랙트의 안전하지 않은 접근 제어 또는 중앙화는 악의적인 행위자가 승인되지 않은 업그레이드를 더 쉽게 수행하도록 만들 수 있습니다.

스마트 컨트랙트 업그레이드 시 고려 사항

  1. 특히 프록시 패턴, 전략 패턴 또는 데이터 분리를 사용하는 경우 승인되지 않은 스마트 컨트랙트 업그레이드를 방지하기 위해 안전한 접근 제어/권한 부여 메커니즘을 사용하세요. 컨트랙트의 소유자만 호출할 수 있도록 업그레이드 함수에 대한 접근을 제한하는 것이 그 예입니다.

  2. 스마트 컨트랙트 업그레이드는 복잡한 활동이며 취약점 도입을 방지하기 위해 높은 수준의 주의가 필요합니다.

  3. 업그레이드 구현 프로세스를 탈중앙화하여 신뢰 가정을 줄이세요. 가능한 전략으로는 업그레이드를 제어하기 위해 다중 서명 지갑 컨트랙트를 사용하거나 DAO 구성원이 업그레이드 승인에 투표하도록 요구하는 것이 있습니다.

  4. 컨트랙트 업그레이드와 관련된 비용에 유의하세요. 예를 들어, 컨트랙트 마이그레이션 중에 이전 컨트랙트에서 새 컨트랙트로 상태(예: 사용자 잔액)를 복사하려면 둘 이상의 트랜잭션이 필요할 수 있으며, 이는 더 많은 가스 수수료를 의미합니다.

  5. 사용자를 보호하기 위해 타임락(timelocks) 구현을 고려하세요. 타임락은 시스템 변경에 적용되는 지연을 의미합니다. 타임락은 다중 서명 거버넌스 시스템과 결합하여 업그레이드를 제어할 수 있습니다. 제안된 작업이 필요한 승인 임계값에 도달하더라도 미리 정의된 지연 기간이 경과할 때까지 실행되지 않습니다.

타임락은 제안된 변경 사항(예: 로직 업그레이드 또는 새로운 수수료 체계)에 동의하지 않는 경우 사용자가 시스템을 종료할 수 있는 시간을 제공합니다. 타임락이 없으면 사용자는 개발자가 사전 통지 없이 스마트 컨트랙트에 임의의 변경 사항을 구현하지 않을 것이라고 신뢰해야 합니다. 여기서 단점은 타임락이 취약점을 신속하게 패치하는 능력을 제한한다는 것입니다.

리소스

오픈제플린 업그레이드 플러그인(OpenZeppelin Upgrades Plugins) - 업그레이드 가능한 스마트 컨트랙트를 배포하고 보호하기 위한 도구 모음입니다.

튜토리얼

더 읽어보기