ERC-2535: 다이아몬드 패턴 이해하기

ERC-2535: 다이아몬드 패턴

Overview

이더리움 블록체인의 스마트 컨트랙트를 개발할 때 가장 큰 제약 중 하나는 컨트랙트 크기 제한(24KB)과 업그레이드 가능성입니다. 이러한 문제를 해결하기 위해 등장한 것이 ERC-2535 다이아몬드 패턴입니다. 이 표준은 모듈식 스마트 컨트랙트 시스템을 구현하기 위한 구조와 방법을 정의합니다.

이 포스팅에서는 ERC-2535 다이아몬드 패턴의 개념, 구성 요소, 이점 및 구현 방법에 대해 알아보겠습니다.

다이아몬드 패턴이란?

다이아몬드 패턴은 2020년 Nick Mudge에 의해 제안된 EIP(Ethereum Improvement Proposal)로, 스마트 컨트랙트를 더 모듈화하고 확장 가능하게 만드는 아키텍처입니다. 전통적인 스마트 컨트랙트와 달리, 다이아몬드 패턴은 컨트랙트를 여러 개의 작은 조각(facet)으로 나누어 관리합니다.

Diamond Pattern

주요 구성 요소

다이아몬드 패턴의 주요 구성 요소는 다음과 같습니다:

  1. 다이아몬드 컨트랙트: 중앙 허브 역할을 하는 코어 컨트랙트로, 함수 호출을 적절한 facet으로 위임(delegate)합니다.
  2. Facet 컨트랙트: 특정 기능을 포함하는 모듈식 컨트랙트입니다. 하나의 다이아몬드는 여러 facet을 가질 수 있으며, 각 facet은 전체 로직의 다른 부분을 담당합니다.
  3. Selector: 함수 선택자는 호출을 올바른 facet으로 라우팅하는 데 사용됩니다. 다이아몬드 컨트랙트는 함수 선택자에서 해당 facet 주소로의 조회 테이블을 유지합니다.
  4. Loupe 함수: 다이아몬드에 대한 정보를 쿼리할 수 있는 내부 검사 함수들로, facet과 그들이 제공하는 함수에 대한 정보를 제공합니다.

ERC-2535의 이점

다이아몬드 패턴을 사용하면 다음과 같은 이점이 있습니다:

  1. 모듈성: 스마트 컨트랙트를 작은 facet으로 나누면 전체 시스템에 영향을 주지 않고 특정 부분을 관리하고 업데이트할 수 있습니다.
  2. 확장성: 다이아몬드는 유기적으로 성장할 수 있는 대규모 복잡한 시스템의 개발을 가능하게 합니다. 새로운 기능은 새로운 facet을 배포하여 추가할 수 있습니다.
  3. 업그레이드 가능성: 개별 facet은 독립적으로 업그레이드될 수 있어 전체 계약을 다시 배포하지 않고도 유연하고 원활한 업데이트가 가능합니다.
  4. 가스 비용 절감: 업데이트되는 facet만 재배포하면 되므로 계약 업그레이드에 대한 전체 가스 비용이 크게 낮아질 수 있습니다.
  5. 조직화: 코드를 facet으로 구성함으로써 개발자는 코드베이스를 깔끔하게 유지하고 탐색하기 쉽게 만들어 유지보수성을 향상시킬 수 있습니다.
  6. 무제한 크기: 다이아몬드는 최대 계약 크기 제한이 없어, 시간이 지남에 따라 추가할 수 있는 기능의 양에 제한이 없습니다.

다이아몬드 패턴 구현하기

1. 개발 환경 설정

먼저 Hardhat을 이용하여 환경을 설정합니다:

npm install --save-dev hardhat

2. 다이아몬드 컨트랙트 생성

다이아몬드 컨트랙트는 중앙 허브 역할을 하며, 함수 호출을 적절한 facet으로 위임하는 로직을 포함합니다:

// Diamond.sol
pragma solidity ^0.8.0;

import "./IDiamondCut.sol";

contract Diamond {
    // 다이아몬드 저장소
    struct Facet {
        address facetAddress;
        bytes4[] selectors;
    }

    mapping(bytes4 => address) public selectorToFacet;

    constructor(IDiamondCut.FacetCut[] memory _diamondCut) {
        // 다이아몬드 컷 구현
    }

    fallback() external payable {
        address facet = selectorToFacet[msg.sig];
        require(facet != address(0), "Function does not exist");
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 {revert(0, returndatasize())}
            default {return(0, returndatasize())}
        }
    }
}

3. Facet 정의

특정 기능을 포함하는 facet 컨트랙트를 생성합니다. 예를 들어, Counter facet을 만들 수 있습니다:

// CounterFacet.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "../libraries/LibDiamond.sol";
import "../libraries/LibCounter.sol";

contract CounterFacet {
    // Access to counter storage
    function getCounterStorage() internal pure returns (LibCounter.CounterStorage storage) {
        return LibCounter.counterStorage();
    }

    // Get the current counter value
    function getCount() external view returns (uint256) {
        return getCounterStorage().count;
    }

    // Increment the counter
    function increment() external {
        getCounterStorage().count += 1;
    }

    // Decrement the counter
    function decrement() external {
        LibCounter.CounterStorage storage cs = getCounterStorage();
        require(cs.count > 0, "Count cannot be negative");
        cs.count -= 1;
    }

    // Set the counter to a specific value
    function setCount(uint256 _count) external {
        LibDiamond.enforceIsContractOwner(); // Only owner can set count
        getCounterStorage().count = _count;
    }
}

4. 다이아몬드와 Facet 배포

다이아몬드 컨트랙트와 facet 컨트랙트를 배포한 후, Diamond Cut 메커니즘을 사용하여 facet을 다이아몬드에 연결합니다:

async function main() {
    const diamondCutFacet = await deploy("DiamondCutFacet", {
        from: deployer,
        log: true,
    });

    // Deploy Diamond
    const diamond = await deploy("Diamond", {
        from: deployer,
        args: [deployer, diamondCutFacet.address],
        log: true,
    });

    // Deploy DiamondInit
    const diamondInit = await deploy("DiamondInit", {
        from: deployer,
        log: true,
    });
    // Deploy facets
    console.log("");
    console.log("Deploying facets");
    const FacetNames = ["DiamondLoupeFacet", "OwnershipFacet", "CounterFacet", "ERC20Facet"];

    const cut: FacetCut[] = [];
    for (const FacetName of FacetNames) {
        const facetDeployment = await deploy(FacetName, {
            from: deployer,
            log: true,
        });

        console.log(`${FacetName} deployed: ${facetDeployment.address}`);
        
        const facetContract = await ethers.getContractAt(FacetName, facetDeployment.address);

        cut.push({
            facetAddress: facetDeployment.address,
            action: FacetCutAction.Add,
            functionSelectors: getSelectors(facetContract) || [],
        });
    }

    const diamondCutContract = await ethers.getContractAt("IDiamondCut", diamond.address);
    const diamondInitContract = await ethers.getContractAt("DiamondInit", diamondInit.address);

    // Call to init function
    let functionCall = diamondInitContract.interface.encodeFunctionData("init");
    const tx = await diamondCutContract.diamondCut(cut, diamondInit.address, functionCall);
}

5. 다이아몬드와 상호작용

배포 후, 다이아몬드 컨트랙트와 상호작용할 수 있습니다. 다이아몬드에 대한 호출은 적절한 facet으로 라우팅됩니다:

    const diamondAddress = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512";

    const counterFacet = await ethers.getContractAt(
        "CounterFacet",
        diamondAddress,
    );

    const count = await counterFacet.getCount();
    console.log("Initial count:", count.toString());

    console.log("Incrementing counter...");
    const incrementTx = await counterFacet.increment();
    await incrementTx.wait();

    const newCount = await counterFacet.getCount();
    console.log("New count:", newCount.toString());

결론

ERC-2535 다이아몬드 패턴은 이더리움에서 모듈식, 확장 가능하고 업그레이드 가능한 스마트 컨트랙트를 구축하기 위한 강력한 프레임워크를 제공합니다. 스마트 컨트랙트를 facet으로 나눔으로써 개발자는 복잡한 시스템을 더 효과적으로 관리하고, 가스 비용을 줄이며, 원활한 업그레이드를 보장할 수 있습니다.

참고 자료