스마트 컨트랙트가 Upgradable 하다는 것

707·2022년 10월 4일
0
post-thumbnail

NFT 생성에 사용되는 ERC721의 확장 컨트랙트로 오픈제플린에서는 ERC721Upgradable.sol 이라는 컨트랙트를 제공해준다.
도대체 Upgradable한 스마트 컨트랙트란 무슨 의미일까?

Proxy Pattern

먼저 확실히 해야할 것은 Upgradable 하다는 것이 컨트랙트의 내용을 바꿀 수 있다는 뜻이 아니라는 것이다. EVM 의 기본 규칙 중 하나는 컨트랙트가 배포되면 변경할 수 없다는 것인데, 대신 업그레이드 가능한 스마트 컨트랙트는 특별한 프록시 패턴을 사용한다.
즉, Upgradable 컨트랙트는 프록시 컨트랙트를 통해 서비스 로직이 담긴 컨트랙트를 연결해주는 방식이다.

사용자가 상호작용 하는 것은 프록시 컨트랙트가 되고, 실제 서비스로직이 담긴 컨트랙트는 별도로 존재한다.
프록시 컨트랙트는 로직컨트랙트의 주소값을 참조하여 바라보게 되는데, 이 때 이 로직컨트랙트를 새롭게 배포하고 새로운 CA값을 프록시컨트랙트에게 알려준다면 클라이언트 측에서는 아무런 코드 변경 없이 새로운 로직으로 처리된 결과값을 받을 수 있을 것이다.

이런 식의 프록시 패턴은 크게 두가지 유형으로 나뉜다.

  1. transparent proxy
  2. UUPS (universal upgradable proxy standard)

다행히도 이 두가지 모두 OpenZeppelin에서 제공이 된다고 한다.

Transparent Proxy

  • 프록시 컨트랙트에서 업그레이드 처리
  • 배포비용이 많이 든다.
  • 유지관리가 쉽다.

UUPS

  • 로직 컨트랙트에서 업그레이드 처리 : 로직 컨트랙트에 업그레이드 관련 메소드를 넣어야함
  • 배포비용이 저렴하다.
  • 유지관리가 비교적 어렵다.
  • Openzeppelin 권장방식!





스마트 컨트랙트를 Upgradable로 바꿔보자

1. 프록시 패턴 적용해주는 컨트랙트 상속

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract ExampleContract is initializable {}

Initializable.sol을 가져와 내 컨트랙트가 initializable 컨트랙트를 상속하도록 해준다. 기본적으로 Transparent Proxy 패턴을 적용해준다.

만약 UUPS 방식의 프록시패턴을 사용하고 싶다면, UUPSUpgradeable 컨트랙트를 추가로 상속해주면 된다. 오픈제플린에서도 UUPS 방식을 권장하고 있으므로 해당 방식으로 계속 진행해보자

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

contract ExampleContractName is initializable, UUPSUpgradable {}


2. constructor 함수 👉 initialize 함수

기존에 우리는 migration을 하며 컨트랙트의 초기값을 할당해주는 constructor 함수를 사용했다. 이를 initialize 함수로 변경해준다.

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

contract ExampleContractName is initializable, UUPSUpgradable {
  // constructor () {} // ❌
  
  function initialize() public initializer {}
}

❗️ 여기서 함수명은 꼭 initialize일 필요가 없지만 제어자로 initializer 를 반드시 붙여줘야 한다!!!



3. 컨트랙트 변경 및 init 함수 사용

만약 기존에 오픈제플린에서 제공해주는 ERC721.sol과 ownerable.sol 컨트랙트를 사용했다면

  1. 컨트랙트를 ~Upgradable이 붙은 버전으로 변경해준다.
  2. 해당 컨트랙트를 상속하는 대신 initialize 함수 내에서 init함수를 실행시킨다.
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
// Upgradable 컨트랙트 가져오기
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import '@openzeppelin-solidity/contract/access/OwnableUpgradable.sol';

contract ExampleContractName is initializable, UUPSUpgradable {
  function initialize() public initializer {
    // init함수 실행
    __ERC721_init(“”);
    __Ownable_init();
    __UUPSUpgradeable_init();
  }
}


4. msg.sender 👉 _msgSender()

이제 프록시패턴을 통해 생성된 로직 컨트랙트는 모든 요청을 프록시컨트랙트를 통해 받게 된다. 이런 경우 클라이언트 측에서 요청을 보낸 address를 확인하기 위해 로직 컨트랙트 내부의 msg.sender를 _msgSender()로 바꿔주어야한다고 한다.



정리

이상으로 일단 Upgradable을 적용하는 방법을 알아보았다.
하지만 Proxy에 관련하여 조금 더 자세히 학습이 필요할 것 같다....
프록시패턴에서 빠지지 않고 나오는 내용이 delegate call인데 아직 이 부분에 대한 개념을 정확히 모른다(ㅠㅠ) 조금 더 공부한 뒤 다음 게시글로 적어봐야겠다.

0개의 댓글