[Solidity] EIP-1967 : Proxy Storage Slots & 배포

임형석·2023년 10월 25일
0

Solidity


EIP-1967

프록시 패턴의 종류슬롯에 대해 기록했던 글이다. 프록시 컨트랙트를 직접 배포하기 전에 알아두면 좋을 내용을 적어두었다.

EIP-1967 은 프록시 스토리지 슬롯의 표준에 대해 설명하고 있다.

프록시와 로직 컨트랙트가 일정한 스토리지 슬롯에 변수를 저장하고, 로직 컨트랙트가 업그레이드 되더라도 기존의 변수가 덮어 씌워지거나 잘못된 스토리지에 저장되지 않도록 안전한 프록시의 표준을 만들었다.


배포 전 준비

먼저, 배포 하기 전 준비물로 Etherscan API 키, Infura API 키, 메타마스크 개인 키가 필요하다.

발급 후 화면 최상단에서 확인. 또는 사진과 같이 모자이크 처리된 부분을 확인.

적절한 App name 을 지어주고, API 발급한 뒤 위 사진의 모자이크 부분에 있는 api 키를 확인.

  • Metamask 의 개인 키 확인 방법

상단 Account 1 오른쪽의 하단 방향 표시 클릭

우측의 계정 세부 정보 클릭

개인 키 표시 클릭 후 확인


배포

배포에는 hardhat 을 사용한다.

프록시의 구조 자체가 복잡하기 때문에 프레임워크를 사용해야 간단하게 배포가 가능하다.

  1. VS code 를 사용하여 배포할 폴더를 생성하고 디렉션을 옮겨준 후,
    콘솔에서 npm init 을 명령어를 실행. package.json 파일을 생성해준다.

  1. package.json 에 아래의 json 코드를 추가해준 후 저장.
    (추가하지 않고, 직접 npm install 명령어로 하나씩 인스톨 해도 됨.)
    "@nomicfoundation/hardhat-ethers": "^3.0.4",
    "@nomiclabs/hardhat-etherscan": "^3.1.7",
    "@openzeppelin/hardhat-upgrades": "^2.3.3",
    "ethers": "^6.8.0",
    "hardhat": "^2.18.2"
  },
  "dependencies": {
    "@openzeppelin/contracts": "^5.0.0",
    "@openzeppelin/contracts-upgradeable": "^5.0.0",
    "dotenv": "^16.3.1"
  }

  1. 콘솔창에 npm i 명령어 실행 후 모든 노드 패키지를 다운로드.

  1. 노드 패키지 다운로드가 끝났다면 콘솔창에 npx hardhat init 명령어 실행. javaScript 프로젝트를 선택.

  1. contracts 폴더에 있는 파일 삭제 후, 다음과 같이 두개의 솔리디티 파일을 생성. 각 파일은 배포할 로직 컨트랙트이며, V1,2 는 각각의 버전이다.

  1. logicV1.sol 파일 컨트랙트 코드를 작성. V2 는 아래에서 다시 작성.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

contract logic_contract_V1 {
    uint public val;
    address public owner;

    function initialize(uint _vals) external {
        val = _vals;
        owner = msg.sender;
    }
}

  1. hardhat.config.js 파일을 아래와 같이 설정. 배포 시 사용할 환경을 아래와 같이 미리 설정해준다. .env 를 사용해 개인 키와 api 키를 가려야 한다. 외부에 노출할 때에는 반드시 키 값을 지운 후에 업로드 해야한다.

(.env 에 저장된 변수를 불러오면 트랜잭션에 문제가 생겨 직접 복붙해서 사용했다.)

require("@nomicfoundation/hardhat-ethers");
require("@openzeppelin/hardhat-upgrades");
require("@nomiclabs/hardhat-etherscan");

const pvkey =
  "개인 키를 복붙하세요";

module.exports = {
  solidity: "0.8.19",
  networks: {
    sepolia: {
      url: `https://sepolia.infura.io/v3/INFURA API 키를 복붙하세요`,
      accounts: [pvkey],
    },
  },
  etherscan: {
    apiKey: "이더스캔 API 키를 복붙하세요",
  },
};

  1. scripts 폴더에 다음과 같이 배포 파일을 생성한 후, V1 코드를 작성.
    ethers 의 getContractFactory 로 컨트랙트 인스턴스를 생성한다.
    인스턴스를 Openzeppelinupgrade.deployProxy 를 사용하여 배포.
    배포와 함께 initialize 한다. 상태변수의 값은 111 로 설정.

const { ethers, upgrades } = require("hardhat");

const main = async () => {
  const logic_V1 = await ethers.getContractFactory("logic_contract_V1");

  const logic_V1s = await upgrades.deployProxy(logic_V1, [111], {
    initializer: "initialize",
  });

  console.log("Contract deployed");
};
main();

  1. 콘솔창에 npx hardhat run --network sepolia scripts/deploy_logicV1.js 명령어 실행. 세폴리아 테스트넷에 배포한다.

  1. 이더스캔에서 트랜잭션을 확인한다. 매 블록마다 트랜잭션이 하나씩. 총 3개의 컨트랙트가 생성 된 것을 확인할 수 있다.
    아래부터 로직, 프록시 오너, 프록시 컨트랙트이다.


  1. 이더스캔에 컨트랙트 코드를 Verify 시켜주어야 한다. npx hardhat verify --network sepolia 컨트랙트 주소 명령어로 이더스캔에 코드를 업로드한다.

다시 이더스캔을 확인하면 컨트랙트 코드가 Verify 되어 있는 것을 확인할 수 있다.


  1. 그리고 이더스캔을 확인한다. 하지만 V1 로직 컨트랙트의 상태변수가 이상하다. 로직 컨트랙트이기에, 상태변수를 저장하지 않는다.


  1. 3개의 컨트랙트 중, 가장 마지막에 배포된 컨트랙트를 확인한다. 컨트랙트 명은 TransparentUpgradeableProxy 이다.

그리고 우측하단의 More Options -> Is this a proxy? 클릭.

proxy 컨트랙트임을 확인하고 Save 를 눌러준 후 다시 컨트랙트 창으로 되돌아간다.

Read as Proxy 라는 버튼이 새로 생기고 상태변수에 접근할 수 있다.
이것이 proxy 컨트랙트이다.


  1. 다음은 로직 컨트랙트를 V2 버전으로 업그레이드 하는 과정이다.
    아래와 같이 V2 로직 컨트랙트 코드를 작성해보았다.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

contract logic_Version_2 {
    uint public val;
    address public owner;
    uint8 public val_2;
    bytes2 public bytee;

    function inc() external {
        val += 1;
    }
}

  1. 컨트랙트 인스턴스를 만든 후, 배포하는 과정이다. V1 과 비슷하지만
    먼저, 이전에 배포했던 프록시 컨트랙트의 주소를 변수로 저장해준다.
    그리고 컨트랙트 인스턴스를 만들고 upgrades.upgradeProxy 를 사용하여 배포한다.

    V1 로직 컨트랙트에서 이미 initialize 했기 때문에, V2 배포 단계에서는 할 필요가 없다.

const { ethers, upgrades } = require("hardhat");

const proxy = "0x0570522F4cae654Ca90476660965eCB489C4d2c7";

const main = async () => {
  const logic_V2 = await ethers.getContractFactory("logic_Version_2");

  await upgrades.upgradeProxy(proxy, logic_V2);

  console.log("Contract upgraded.");
};

main();

  1. 콘솔창에 npx hardhat run --network sepolia scripts/deploy_logicV2.js 명령어 실행으로 배포한다.

    두개의 컨트랙트가 배포되었다. 아래부터 로직, 프록시 어드민 컨트랙트이다.
    프록시 컨트랙트는 다시 배포되지 않는다. 상태변수가 담겨있는 스토리지기 때문이다.

    배포 확인 후, 로직 컨트랙트의 코드를 Verify 한다.
    npx hardhat verify --network sepolia 컨트랙트 주소


  1. 다시 프록시 컨트랙트로 돌아가보면, 상태변수가 V2 로직 컨트랙트에 맞게 접근이 가능하다.

유저가 프록시 컨트랙트를 사용한다고 가정했을 때, 상태변수가 바뀔 수 있는지도 확인한다.

Write as Proxy 를 클릭, Inc 함수를 call 하여 val 상태변수를 +1 해본다.

트랜잭션이 컨펌되고 다시 val 상태변수를 확인. 112 로 1 증가했다.


정리

업그레이더블 프록시 컨트랙트를 배포해보았다.

배포한 프록시 패턴은 상속 스토리지 패턴으로, 아래와 같은 특징이 있다.

상속 스토리지 패턴
프록시 컨트랙트가 유지하고 있는 스토리지 구조를 로직 컨트랙트가 그대로 사용하여 충돌이 없다는 것이 가장 큰 장점.
하지만, 업그레이드 시에도 구조를 유지해야 하기에 불필요한 상태 변수가 추가될 수 있다는
점과 배포 후 초기화가 필요하다는 점이 단점이다.


아래는 로직 컨트랙트의 상태변수를 나타낸 것이다.

  • 로직 v1 상태변수
    uint public val;
    address public owner;
  • 로직 v2 상태변수
    uint public val;
    address public owner;
    uint8 public val_2;
    bytes2 public bytee;

상태변수가 새로 선언됨에 따라 프록시 컨트랙트도 이에 맞게 업그레이드가 되었다.

배포할 때 두 번째 상태변수를 address public ow; 라고 작성하고 배포해보니 아래와 같이 에러가 났다.

오픈제플린의 컨트랙트에서 발생한 에러로, 기존의 스토리지 레이아웃과 다르다면 미리 에러를 띄워주며 배포 후에 발생할 끔찍할 상황을 막아준다.

또한, 다음과 같이 V2 로직 컨트랙트의 스토리지 레이아웃을 바꿔버리면...

uint public val;
address public addr;
bytes2 public bytee;
uint8 public val_2;
address public owner;

스토리지 레이아웃 에러가 발생한다. 대신 에러 상황도 자세히 설명해주는데, 새 변수들은 상속된(기존의) 변수들의 뒤에 선언되어야 한다고 설명한다.

프록시를 배포할 땐 하드햇을 사용해서 미리 에러를 확인한 후 배포할 수 있도록 하는 것이 좋겠다. 문제 있는 상태로 배포된다면 돌이킬 수 없다.
따라서 리믹스는 사용금지.


또한, V1 로직 컨트랙트의 initialize 함수를 여러번, 누구나 호출이 가능하다. 만약 누군가가 이 함수를 호출해버린다면 기존의 프록시 컨트랙트에 저장된 상태변수가 모두 초기화 되어버린다. 따라서, 두번 이상 호출할 수 없게 수정하는 작업이 필요하다.


하드햇을 사용하여 생각보다 간단하게 배포되었지만, 하드햇의 설정이 생각보다 까다로웠다. 특히 트랜잭션 생성 시 .env 의 환경변수 값이 읽히지 않아 몇시간 동안 고생하기도 했다. 실제 프로젝트가 아니므로 환경변수 사용없이 배포하는 것을 추천한다.....


0개의 댓글