ERC-8153 패싯 기반 다이아몬드 (Facet-Based Diamonds)
Final · 기타 · 2026-02-07 작성
여러 구현 컨트랙트(패싯)를 하나의 주소로 묶는 다이아몬드 컨트랙트에서, 각 패싯이 자기 함수 목록을 스스로 알려 주게 해 배포와 업그레이드를 더 단순하고 저렴하게 만드는 표준입니다.
왜 필요한가
다이아몬드는 하나의 컨트랙트 주소로 여러 구현 컨트랙트의 기능을 제공하는 구조로, ERC-2535에서 처음 표준화되었습니다. 그런데 기존 방식에서는 다이아몬드를 배포하거나 업그레이드할 때 각 함수의 식별자(함수 셀렉터)를 체인 밖에서 일일이 모아 전달해야 했습니다. Hardhat이나 Foundry 같은 일반 개발 도구는 이 셀렉터 관리를 기본으로 지원하지 않아서 개발자는 별도 스크립트나 서드파티 라이브러리에 의존해야 했습니다. 또 셀렉터 목록을 호출 데이터로 넘기고 저장하는 과정에서 가스 비용도 커졌습니다. 이 표준은 패싯이 자신의 셀렉터를 온체인에서 직접 알려 주는 exportSelectors() 함수를 의무화해, 셀렉터 수집을 체인 위로 옮기고 배포·업그레이드를 패싯 단위로 처리하게 합니다. 그 결과 오프체인 셀렉터 관리가 사라지고 배포가 더 결정적이며 가스 효율도 좋아집니다.
구현해야 하는 것
function facets() external view returns (Facet[] memory facets_);: 다이아몬드가 쓰는 모든 패싯 주소와 각 패싯의 4바이트 함수 셀렉터 목록을 돌려줍니다.function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);: 특정 패싯이 지원하는 모든 함수 셀렉터를 돌려줍니다.function facetAddresses() external view returns (address[] memory facetAddresses_);: 다이아몬드가 쓰는 모든 패싯 주소를 돌려줍니다.function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);: 주어진 셀렉터를 처리하는 패싯 주소를 돌려줍니다. 없으면 0 주소를 돌려줍니다.function exportSelectors() external pure returns (bytes memory selectors);: 패싯이 구현한 함수 셀렉터들을 4바이트씩 이어 붙인 bytes로 돌려줍니다. 모든 패싯이 반드시 구현해야 합니다.event FacetAdded(address indexed _facet);: 패싯이 다이아몬드에 추가될 때 발생합니다.event FacetReplaced(address indexed _oldFacet, address indexed _newFacet);: 기존 패싯이 새 패싯으로 교체될 때 발생합니다.event FacetRemoved(address indexed _facet);: 패싯이 다이아몬드에서 제거될 때 발생합니다.event DiamondDelegateCall(address indexed _delegate, bytes _delegateCalldata);: 다이아몬드의 생성자나 패싯 함수가 delegatecall을 수행할 때 발생하는 선택 이벤트입니다.event DiamondMetadata(bytes32 indexed _tag, bytes _data);: 버전 같은 임의의 메타데이터를 기록하는 선택 이벤트입니다.
심층 설명
다이아몬드 구조와 fallback 라우팅
다이아몬드는 외부 함수 호출을 여러 구현 컨트랙트(패싯)로 라우팅하는 프록시 컨트랙트이며 모든 영속 데이터는 다이아몬드의 저장소에 있습니다. 외부 함수가 호출되면 fallback 함수가 실행되어 calldata의 첫 4바이트(함수 셀렉터)로 해당 패싯을 찾고 delegatecall로 실행합니다. 이때 msg.sender와 msg.value는 바뀌지 않고 다이아몬드의 저장소만 읽고 씁니다. 셀렉터에 대응하는 패싯이 없고 기본 함수 같은 다른 처리 장치도 없으면 FunctionNotFound(bytes4 _selector) 에러로 반드시 되돌려야 합니다.
exportSelectors()의 규격
모든 패싯은 pure 함수인 exportSelectors()를 반드시 구현해야 합니다. 반환값은 4바이트 셀렉터를 이어 붙인 bytes이며 길이는 반드시 4의 배수여야 하고 같은 셀렉터를 두 번 이상 담으면 안 됩니다. 다이아몬드는 배포나 업그레이드 시점에 각 패싯의 이 함수를 호출해 어떤 셀렉터를 추가·교체·제거할지 온체인에서 결정합니다. bytes4[] 대신 bytes를 쓰는 이유는 메모리 효율 때문입니다. bytes4[]는 원소당 32바이트를 차지해 87.5%가 낭비되지만 bytes로 압축하면 오버헤드가 줄고 패싯 쪽에서는 bytes.concat으로 간결하게 구현할 수 있습니다.
패싯 기반 이벤트
이 표준은 ERC-2535의 함수 단위 이벤트를 패싯 단위 이벤트로 바꿉니다. 패싯이 추가·교체·제거될 때 각각 FacetAdded, FacetReplaced, FacetRemoved를 반드시 발생시켜야 하며 패싯 변경 하나마다 정확히 하나의 이벤트가 나와야 합니다. 블록 탐색기 같은 도구는 이벤트에 담긴 패싯 주소에 exportSelectors()를 호출해 함수 셀렉터를 얻습니다. 선택 이벤트로는 fallback 라우팅이 아닌 delegatecall을 기록하는 DiamondDelegateCall과, 버전 같은 임의 정보를 남기는 DiamondMetadata가 있습니다. 이 둘은 upgradeDiamond를 구현하는 경우에는 표준이 정한 조건에서 반드시 발생시켜야 합니다.
upgradeDiamond의 동작 규칙
upgradeDiamond는 선택 사항이며 도구와의 상호운용을 위해 규격화되었습니다. 추가, 교체, 제거의 순서로 실행하고 교체 시에는 옛 패싯과 새 패싯의 exportSelectors()를 비교해 새 패싯에만 있는 셀렉터는 추가, 둘 다 있는 것은 새 패싯으로 교체, 옛 패싯에만 있는 것은 제거합니다. _delegate가 0이 아니면 _delegateCalldata로 delegatecall을 수행해 상태 초기화나 이전을 하고 DiamondDelegateCall 이벤트를 발생시킵니다. delegatecall이 실패하면 받은 revert 데이터를 그대로 되돌리거나, 데이터가 없으면 DelegateCallReverted로 되돌립니다. _tag가 0이 아니거나 _metadata가 비어 있지 않으면 DiamondMetadata 이벤트를 발생시킵니다. 패싯을 바꾸지 않고 delegatecall만 하려는 호출도 유효합니다.
업그레이드 실수를 막는 에러 조건
upgradeDiamond 구현은 다음 상황을 감지해 지정된 에러로 되돌려야 합니다. 이미 있는 셀렉터를 추가하면 CannotAddFunctionToDiamondThatAlreadyExists, 없는 패싯을 제거하면 CannotRemoveFacetThatDoesNotExist, 패싯을 자기 자신으로 교체하면 CannotReplaceFacetWithSameFacet, 없는 패싯을 교체하면 FacetToReplaceDoesNotExist, 교체 대상이 아닌 다른 패싯에 매핑된 셀렉터를 교체하면 CannotReplaceFunctionFromNonReplacementFacet입니다. 또 패싯 주소에 바이트코드가 없으면 NoBytecodeAtAddress, exportSelectors() 호출이 실패하면 ExportSelectorsCallFailed, 셀렉터가 0개면 NoSelectorsForFacet으로 되돌려야 합니다. 서로 다른 함수 시그니처가 같은 4바이트 셀렉터를 갖는 충돌도 '이미 있는 셀렉터'로 처리되어 막힙니다.
가스 절감의 원리
기존 방식은 셀렉터를 bytes4[] 배열로 생성자에 넘겨 calldata 비용을 내고 메모리로 복사하는 비용도 들었습니다. 패싯 기반 다이아몬드는 패싯 주소만 넘기고 셀렉터는 온체인에서 얻어 이 비용을 피합니다. 저장소 측면에서도, 기존에는 셀렉터를 직접 저장해 함수 수에 비례해 슬롯이 늘었지만 패싯 기반에서는 조회 데이터를 함수가 아닌 패싯 단위로 저장할 수 있습니다. 패싯당 대표 셀렉터 하나만 있으면 되므로 32바이트 슬롯 하나가 패싯 8개까지 담을 수 있고 패싯 연결 리스트로 구현하면 슬롯 하나로 패싯과 셀렉터 수에 제한 없이 조회를 지원할 수 있습니다.
불변 다이아몬드와 자체 업그레이드 함수
업그레이드 함수는 필수가 아니므로 다이아몬드를 생성자 안에서 모두 구성하고 업그레이드 함수를 넣지 않으면 배포와 동시에 불변이 됩니다. 처음에는 업그레이드 가능하게 두었다가 나중에 업그레이드 함수를 제거해 불변으로 만들 수도 있습니다. 표준이 정한 upgradeDiamond 대신 자체 업그레이드 함수를 만들어도, 패싯 추가·교체·제거 이벤트를 발생시키고 조회 함수들이 정확한 정보를 계속 돌려주면 표준을 준수하는 것입니다. 저장소 배치 방식은 이 표준이 지정하지 않으며 ERC-8042 다이아몬드 스토리지나 ERC-7201 네임스페이스 스토리지 같은 패턴을 쓸 수 있습니다.
ERC-2535와의 호환성
이 표준의 다이아몬드는 ERC-2535와 같은 조회 함수를 구현하므로 그 함수에 의존하는 ERC-2535 도구와 호환됩니다. 그러나 이벤트와 업그레이드 방식의 호환성은 깨집니다. 또 ERC-2535용으로 배포된 기존 패싯은 exportSelectors() 함수가 없으면 이 표준의 다이아몬드에서 쓸 수 없습니다. 이 제안의 상태는 Final로 확정되었습니다.
쓸 때 주의할 점
- upgradeDiamond 접근 제한: upgradeDiamond는 delegatecall을 통해 다이아몬드의 저장소에 접근하는 임의 실행을 허용하므로, 이 함수에 대한 접근은 신중하게 제한해야 합니다.
- 신뢰되고 검증된 패싯만 사용: 신뢰되고 검증된 패싯만 추가해야 합니다. exportSelectors()는 반드시 pure여야 하고 반환값이 달라지는 로직을 담지 않아야 하며 패싯은 불변이어야 합니다. 반환 셀렉터가 바뀌면 업그레이드가 잘못된 셀렉터를 추가·제거·교체해 다이아몬드를 손상시킬 수 있습니다.
- 업그레이드 무결성 검사: 규격대로 구현된 upgradeDiamond는 이미 있는 패싯 추가, 없는 패싯 교체·제거, 이미 있는 셀렉터 추가, 바이트코드 없는 주소, 셀렉터 0개 등 여러 실수를 revert로 막습니다. 셀렉터 충돌도 '이미 존재하는 셀렉터'로 처리되어 차단됩니다.
- selfdestruct 사용 금지 권고: 패싯에서 selfdestruct를 쓰는 것은 강하게 권장되지 않습니다. 잘못 쓰면 다이아몬드나 패싯이 삭제될 수 있습니다.
- 투명성 확보: 패싯이 추가·교체·제거될 때마다 이벤트가 발생하고 소스 코드를 검증할 수 있어, 사람과 소프트웨어가 다이아몬드의 변경을 감시하고 전문가가 업그레이드 이력을 검토할 수 있습니다.