본문 바로가기
블록체인 backEnd

업그레이더블 컨트랙트 샘플

by gun_poo 2023. 8. 28.

업그레이더블 컨트랙트에 대해 이전에 알아보았다. 하여 erc20 샘플을 만들고 관련 하드햇 테스트 코드까지 작성해서 마무리 하였다.

이를 정리한다.

1. Contract 종류

  • Erc20ImplV1
  • proxy
  • storage
  • Erc20ImplV2

1-1. Erc20ImplV1

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./bittoToken.storage.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

contract ERC20Impl is ERC20Upgradeable, ReentrancyGuardUpgradeable, PausableUpgradeable, OwnableUpgradeable, AccessControlUpgradeable, bittoTokenstorage {

    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    // 초기화 함수: 컨트랙트 생성 후 한 번만 호출되어야 한다.
    function initialize(
        string memory name,
        string memory symbol,
        uint256 initialSupply,
        address _roleAdmin
    ) public initializer {
        __ERC20_init(name, symbol);          // ERC20 토큰 초기화
        __ReentrancyGuard_init();            // ReentrancyGuard 초기화
        __Pausable_init();                   // Pausable 초기화
        __Ownable_init();                    // Ownable 초기화
        __AccessControl_init();              // AccessControl 초기화

        _mint(_msgSender(), initialSupply);  // 발행자에게 초기 공급량 전송

        _setupRole(DEFAULT_ADMIN_ROLE, _roleAdmin);
        _setupRole(MINTER_ROLE, _roleAdmin);
    }

    function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
        _mint(to, amount);
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function _beforeTokenTransfer(address from, address to, uint256 amount) internal whenNotPaused override {
        super._beforeTokenTransfer(from, to, amount);
    }
}

 

주요기능 정리

  1. 권한 관리(Access Control): 컨트랙트는 OpenZeppelin AccessControlUpgradeable 라이브러리를 사용하여 권한 관리를 수행한다. MINTER_ROLE이라는 역할(role) 정의되어 있으며, 해당 역할이 부여된 계정만 토큰 발행(mint) 권한을 가진.
  2. 일시 중지(Pausable): PausableUpgradeable 라이브러리를 활용하여 일시 중지(pause) 해제(unpause) 기능을 제공한다. 일시 중지 상태에서는 토큰 전송(transfer) 등의 작업이 불가능하다.
  3. 재진입 방어(Reentrancy Guard): ReentrancyGuardUpgradeable 라이브러리를 활용하여 재진입 공격(reentrancy attack)으로부터 보호한다. nonReentrant 한정자(modifier)를 제공하여 이런 종류의 공격으로부터 컨트랙트를 보호한다.
  4. 소유자 관리(Ownability): OwnableUpgradeable 라이브러리로 소유자(owner) 개념과 그에 따른 권한 관리를 수행한다.
  5. 초기화 함수: initialize 함수에서 각각의 모듈들(__ERC20_init, __ReentrancyGuard_init 등등) 함께 초기 공급량(_mint), 역할 설정(_setupRole), 필요한 초기화 작업들을 수행한.
  6. Upgradable: 위에 언급된 모든 OpenZeppelin 라이브러리들은 'upgradeable' 버전이다, 프록시 패턴(proxy pattern) 사용해서 로직 코드와 데이터 저장소가 분리된 형태로 스마트 컨트랙트가 배포되므로 추후에 로직 코드 부분만 업그레이드 가능함을 의미.

또한 이 컨트랙트는 storage라는 스토리지 계약과 연결되어 있다. 이곳에는 상태 변수들(state variables)과 같은 데이터가 저장되며, 로직 코드와 분리되어 관리된다.

 

여기서 재진입 방어는 마지막 함수인 

// 토큰 전송 전 호출되는 내부 훅(hook) 함수: Pausable의 paused 상태를 확인합니다.
    function _beforeTokenTransfer(address from, address to, uint256 amount) internal whenNotPaused override {
        super._beforeTokenTransfer(from, to, amount);
    }

에 사용되었다.

_beforeTokenTransfer 함수는 토큰을 전송하기 전에 호출되는 내부 훅(hook) 함수이다.

이 함수는 PausableUpgradeable 라이브러리에서 제공하는 whenNotPaused 한정자를 사용하여 일시 중지 상태인지 확인한다.

그리고 super._beforeTokenTransfer(from, to, amount)를 호출하여 ERC20 토큰의 전송 전처리 로직을 실행한다.

여기서 ReentrancyGuardUpgradeable 라이브러리가 사용되는 부분은 _beforeTokenTransfer 함수가 재진입 공격(reentrancy attack)으로부터 보호되도록 하는 것이다. 이 라이브러리는 재진입을 방지하기 위해 mutex(상호 배제) 패턴을 구현하고 있으며, 한 번에 하나의 함수만 실행할 수 있도록 제한한다.

따라서 _beforeTokenTransfer 함수 내부에서 ReentrancyGuardUpgradeable 라이브러리의 nonReentrant 한정자가 활용되어 재진입 공격으로부터 컨트랙트를 보호하고 있다.

 

 

1-2. proxy contract

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

contract ERC20Proxy is TransparentUpgradeableProxy {
    constructor(address _logic, address _admin, bytes memory _data) TransparentUpgradeableProxy(_logic, _admin, _data) {
    }
     function getAdmin() public view returns (address) {
        return _admin();
    }

    function getImplementation() public view returns (address) {
        return _implementation();
    }
}

OpenZeppelin 라이브러리의 TransparentUpgradeableProxy 컨트랙트를 상속 받는다.

트랜스페어런트 업그레이드 프록시(Transparent Upgradeable Proxy)는 프록시 패턴을 사용해 컨트랙트의 로직을 변경하거나 업그레이드 하는 데 사용되는 디자인 패턴이다.

 

파라미터로 _logic, _admin, _data를 받는 생성자 함수를 통해 프록시를 초기화한다.

 

  1. _logic 파라미터는 프록시 컨트랙트가 대리할 로직 컨트랙트의 주소. 프록시 패턴은 로직 컨트랙트에 속한 함수를 호출할 것처럼 프록시 컨트랙트를 호출하여 로직 컨트랙트의 함수를 간접적으로 실행한다.
  2. _admin 파라미터는 프록시 컨트랙트를 관리할 수 있는 관리자의 주소이다. 관리자는 프록시가 대리할 로직 컨트랙트 주소를 업그레이드 할 수 있는 권한을 가진다.
  3. _data 파라미터는 프록시 컨트랙트가 로직 컨트랙트를 초기화 할 때 사용하는 데이터. 예를 들어, 로직 컨트랙트의 생성자 함수에 필요한 파라미터를 이 _data에 포함시킬수 있다.

 

그리고 이 ERC20Proxy 컨트랙트는 getAdmin()함수와 getImplementation()함수를 통해 프록시 컨트랙트의 관리자 정보와 현재 대리중인 로직 컨트랙트의 주소 정보를 확인할 수 있게 해준다.

이렇게 프록시 패턴을 사용하면 스마트 컨트랙트를 업그레이드하더라도 주소는 변하지 않으므로 사용자나 다른 컨트랙트가 활용하는 영향을 최소화하면서 지속적으로 컨트랙트를 개선하거나 버그를 수정하는 작업을 가능하게 해준다.

 

1-3. storage contract

보통의 업그레이더블 컨트랙트와 달리 storage contract 를 추가해준다.

데이터(상태 변수)는 bittoTokenstorage에 저장된다. 이렇게 하면 로직과 데이터가 분리되어, 나중에 필요에 따라 로직 코드만 업그레이드하거나 변경할 수 있다.

따라서 이 Impl, sotrage는 함께 작동하여 프록시 패턴(proxy pattern)으로 알려진 upgradable 스마크컨터랙 패턴을 구현한다:

  • storage: 상태 변수를 저장하는 곳
  • ERC20Impl: 비즈니스 로직(ERC20 토큰 기능 등)을 담당

데이터와 비즈니스 로직이 분리된 결과로써, 비즈니스 로직만 변경하더라도 데이터는 영향 받지 않아 안전하게 유지된다.

 

 

필요한 컨트랙트들을 소개하였고 다음 게시물에서 이 컨트랙트들이 어떻게 상호작용을 하는지

작성한 하드햇 테스트코드를 토대로 정리하겠다.

'블록체인 backEnd' 카테고리의 다른 글

Pool : first provide Liquidity  (0) 2023.10.08
DeFi Contract 작업  (0) 2023.10.03
database mysql sequelize setting  (1) 2023.09.03
업그레이더블 컨트랙트 hardhat testCode.01  (0) 2023.08.28
브릿지  (0) 2023.01.26

댓글