보다 더 생생한 기록

[Solidity] 업그레이더블 컨트랙트(Upgradable Contract) 본문

블록체인

[Solidity] 업그레이더블 컨트랙트(Upgradable Contract)

viviviviviid 2024. 6. 16. 21:04
업그레이더블 컨트랙트를 이용해서 간단한 프로젝트를 만들기전에, 제대로 짚고 넘어가보자.

 

업그레이더블 컨트랙트(Upgradable Contract)

:  한번 배포하면 수정할 수 없는 불변성의 특징을 가진 스마트컨트랙트를 업그레이드 시킬 수 있는 방식으로 설계한 스마트컨트랙트

 

프록시 패턴(Proxy Pattern)

: 실제 로직 컨트랙트(Logic Contract)을 대리하는 역할을 하는 프록시 컨트랙트

 사용자는 프록시 컨트랙트와 상호 작용하지만, 실제 실행되는 로직은 별도의 논리 컨트랙트에서 수행

그림1 : 유저 - 프록시 컨트랙트 - 로직 컨트랙트 관계

 

위 그림처럼 사용자가 프록시 컨트랙트의 함수를 호출하면, 프록시 컨트랙트에 저장된 주소를 이용하여 로직 컨트랙트의 함수를 호출할 수 있도록 한다.

 

위 그림1만 보면 다른 컨트랙트의 함수를 불러와 사용하는 것과 뭐가 다른가 싶을 수 있다.

 

Call vs DelegateCall

차이점은 State가 달라지는 위치에 있다. 아래와 같이 컨트랙트 A와 B가 있고, A에서는 B의 함수를 호출하는 기능이 있다고 하자.

그림1을 적용하면, 컨트랙트 A가 Proxy Contract, 컨트랙트 B가 Logic Contract이다.

A와 B 컨트랙트. B에는 state를 변화시켜주는 함수가 있고, A에서는 그 함수를 호출한다.

 

일반적인 Call로 B 컨트랙트의 함수를 호출하면, B 컨트랙트의 state가 1로 변경되고 A 컨트랙트의 state는 0으로 그대로이다.

일반적인 Call로 인한 state 변화

 

이번엔 업그레이더블 컨트랙트에 사용되는 DelegateCall로 타 컨트랙트의 함수를 호출하면, A 컨트랙트의 state가 1로 변경되고 B컨트랙트의 state는 0으로 그대로이다.

DelegateCall로 인한 state 변화

 

이를 이용하면, 추후 state를 변화시키는 알고리즘이 바뀌어야할때, 메인인 컨트랙트 A를 재 배포할 필요 없이 로직을 담당하는 B 만 갈아치워도 되는 장점이 있다. 이렇게 불변성의 한계를 극복하고 기능을 바꿀 수 있는 점 때문에 Upgradable Contract 라고 한다.

 

DelegateCall 그림 하나로 정리


스토리지 레이아웃(Storage Layout)

조금 더 딥하게 들어가기 전에 스토리지 레이아웃을 알고가야한다.

우리가 솔리디티로 컨트랙트를 작성할 때, 여러 가지 상태 변수를 선언하고 사용하게 된다. 이때 상태 변수들이 저장되는 방식을 스토리지 레이아웃이라고 한다. 업그레이더블 컨트랙트를 사용할 때 스토리지 레이아웃은 매우 중요한 개념이다. 왜냐하면 잘못된 스토리지 레이아웃 관리로 인해 데이터 손실이나 예기치 않은 오류가 발생할 수 있기 때문이다.

 

스토리지 슬롯 정해지는 원리

솔리디티에서 상태 변수들은 특정한 방식으로 저장된다. 상태 변수는 스토리지 슬롯이라는 고정된 위치에 저장되며, 각 슬롯은 256비트의 공간을 가진다.

  1. 상태 변수의 선언 순서: 상태 변수는 선언된 순서대로 슬롯에 할당된다. 예를 들어, 첫 번째 상태 변수는 슬롯 0에, 두 번째 상태 변수는 슬롯 1에 저장된다.
  2. 슬롯 내 배치: 256비트 이하의 변수들은 한 슬롯에 여러 개가 배치될 수 있다. 예를 들어, uint128 변수 두 개는 한 슬롯에 들어갈 수 있다.
  3. 구조체와 배열: 구조체와 배열은 특별한 슬롯 배치 규칙을 따른다. 구조체의 각 필드는 순서대로 슬롯에 저장되며, 배열은 시작 슬롯만 저장되고 실제 데이터는 그 뒤에 연속적으로 저장된다.

스토리지 변수 위치 선정 (별표 다섯개)

업그레이더블 컨트랙트를 설계할 때는 스토리지 변수의 위치를 신중하게 선정해야 한다. 프록시 컨트랙트는 상태 변수를 저장하고, 로직 컨트랙트는 함수 로직만을 담기 때문이다. 따라서 로직 컨트랙트를 업그레이드하더라도 프록시 컨트랙트의 스토리지 레이아웃은 유지되어야 한다.

  • 업그레이드 시 주의사항:
    • 새로운 상태 변수를 추가할 때는 기존 상태 변수의 순서를 변경하지 않아야 한다.
    • 가능한 한 새로운 상태 변수는 기존 상태 변수 뒤에 추가해야 한다.
    • 상태 변수의 타입을 변경하면 안 된다. 타입을 변경하면 슬롯 배치가 달라져 예기치 않은 동작이 발생할 수 있다.
컨트랙트 A와 컨트랙트 B가 공유하는 변수의 순서(슬롯 번호)는 동일해야한다.

 

스토리지 충돌

프록시 컨트랙트와 로직 컨트랙트 간의 스토리지 충돌은 심각한 문제를 야기할 수 있다. 예를 들어, 프록시 컨트랙트가 가지고 있는 상태 변수를 로직 컨트랙트가 덮어쓰게 되면 데이터가 손상될 수 있다. 이를 방지하기 위해 프록시 컨트랙트는 상태 변수의 이름이나 타입을 변경하지 않고 유지해야 한다.

 

프록시 패턴과 constructor 사용 불가

프록시 패턴을 사용할 때 주의할 점은 로직 컨트랙트에 constructor를 사용할 수 없다는 것이다. 대신 초기화 로직은 별도의 initialize 함수로 구현해야 한다. 이는 프록시 컨트랙트가 로직 컨트랙트의 constructor를 호출할 수 없기 때문이다.

 

fallback 누수를 막기 위한 UUPS

UUPS(Upgradeable Proxy) 패턴은 업그레이더블 컨트랙트의 보안성을 강화하기 위해 도입된 방식이다. UUPS는 프록시 컨트랙트가 잘못된 로직 컨트랙트를 참조하지 않도록 하기 위해 upgradeTo 함수를 통해 업그레이드를 관리한다. 이 함수는 권한이 있는 관리자만 호출할 수 있으며, 이를 통해 프록시 컨트랙트의 fallback 함수가 예상치 못한 주소로 delegatecall을 하지 않도록 방지한다.

 

 

자 이제 이 내용들을 기반으로 업그레이더블 컨트랙트를 구축하고 하드햇으로 테스트까지 진행해보자.