ERC-6224 컨트랙트 의존성 레지스트리 (Contracts Dependencies Registry)

Last Call · 기타 · 2022-12-27 작성

프로토콜이 쓰는 모든 스마트 컨트랙트의 주소를 한 레지스트리에 이름으로 등록해 두고 각 컨트랙트가 필요한 의존 컨트랙트를 거기서 받아 오게 하는 관리 표준입니다. 등록된 컨트랙트는 프록시를 씌워 업그레이드할 수도 있습니다.

왜 필요한가

이더리움 생태계가 커지면서 프로젝트는 점점 복잡해지고 새 기능을 계속 내놓으려면 유연하게 바꿀 수 있어야 합니다. 그런데 블록체인과 스마트 컨트랙트는 한번 배포하면 바꿀 수 없는 성질이 있어 이 요구를 맞추기 어렵습니다. 복잡해진 시스템에서 컨트랙트끼리 서로를 참조하는 관계가 얽히면 버그가 생기기 쉽고 유지보수도 힘들어집니다. 어떤 컨트랙트가 어떤 컨트랙트를 쓰는지가 코드 곳곳에 흩어져 있으면, 하나를 교체할 때 그 주소를 들고 있는 모든 컨트랙트를 일일이 찾아 고쳐야 합니다. 이 표준은 주소를 한곳에 모아 두는 레지스트리(ContractsRegistry)와 의존성을 주입받는 믹스인(Dependant) 두 컨트랙트로 이 문제를 풉니다. 명확한 구조, 실행 중 컨트랙트 추가·제거·교체, 필요할 때 켤 수 있는 업그레이드, 투명한 의존 관계가 장점으로 제시됩니다.

구현해야 하는 것

심층 설명

ContractsRegistry의 역할과 필수 규칙

ContractsRegistry는 프로토콜 안에서 쓰이는 모든 독립(standalone) 컨트랙트의 참조를 반드시 저장해야 하는 싱글톤 레지스트리입니다. 등록 시 선택한 프록시 컨트랙트를 위에 배포하도록 구성할 수 있습니다(MAY). 제로 주소 등록은 반드시 거부해야 하고 getContract는 요청한 컨트랙트가 address(0)이면 반드시 revert해야 합니다. 이는 프로토콜 초기화 실수의 위험을 줄이기 위한 것으로, 올바른 컨트랙트를 의존성 주입 전에 먼저 등록해 두어야 합니다. addContract, addProxyContract, addProxyContractAndCall, justAddProxyContract도 같은 이유로 제로 주소에서 revert합니다.

등록 함수 세 갈래

addContract는 프록시 없이 쓰는 컨트랙트용으로, 시스템이 업그레이드 권한을 갖지 않거나 설계상 업그레이드가 불가능한 컨트랙트를 등록합니다. addProxyContract와 addProxyContractAndCall은 전달받은 구현 위에 프록시를 새로 배포해 등록하며 레지스트리가 업그레이드할 수 있어야 하는 컨트랙트에 씁니다. AndCall 형태는 배포 직후 프록시를 호출할 데이터(예: ABI 인코딩된 초기화 호출)를 받습니다. justAddProxyContract는 이미 배포된 프록시를 그대로 등록하는 함수로, 시스템이 새 ContractsRegistry로 이전할 때 쓸 수 있고 이때 새 레지스트리는 그 프록시를 업그레이드할 권한을 가져야 합니다.

의존성 주입 메커니즘

injectDependencies(name)는 등록된 컨트랙트의 setDependencies()를 address(this)와 bytes("")를 인자로 반드시 호출해야 하고 injectDependenciesWithData는 bytes("") 대신 전달받은 data를 넘깁니다. data 매개변수는 애플리케이션별 추가 맥락을 전달하기 위한 것으로 메서드 동작을 확장하는 데 쓸 수 있습니다. 모든 독립 컨트랙트는 Dependant를 상속해야 하며 필요한 의존성은 constructor나 initializer가 아니라 오버라이드한 setDependencies 메서드에서 설정해야 합니다. 이렇게 하면 의존 대상이 바뀌어도 주입을 다시 실행하는 것으로 참조를 갱신할 수 있습니다.

인젝터 접근 제어와 저장 슬롯

setDependencies와 setInjector는 인젝터(injector)만 호출할 수 있어야 합니다. 최초 인젝터는 제로 주소이며 이 경우 접근 제어 검사에서 revert하지 않아야 합니다. 참조 구현의 _checkInjector는 injector가 address(0)이거나 msg.sender와 같을 때만 통과시킵니다. 인젝터 주소(보통 ContractsRegistry)는 특수 슬롯 0x3d1f25f1ac447e55e7fec744471c4dab1c6a2b6ffb897825f9ea3d2e8c9be583에 저장할 수 있으며(MAY), 이 값은 bytes32(uint256(keccak256("eip6224.dependant.slot")) - 1)로 얻습니다. setInjector가 external인 이유는 팩토리로 찍어낸 컨트랙트에도 의존성 주입을 지원하기 위해서이며 각별히 주의해서 써야 한다고 명시합니다.

업그레이드 경로와 참조 구현

upgradeContract와 upgradeContractAndCall은 등록된 프록시를 새 구현으로 올리며 ProxyContractUpgraded 이벤트를 반드시 내야 합니다. 구현 간 호환성 확인은 소유자(Owner)의 책임입니다. 참조 구현(OpenZeppelin 4.9.2 기반)에서는 레지스트리가 초기화 때 ProxyUpgrader를 배포하고 TransparentUpgradeableProxy의 admin으로 이 ProxyUpgrader를 지정합니다. 업그레이드 요청은 onlyOwner 레지스트리 함수를 거쳐 ProxyUpgrader.upgrade로 실행되고 프록시로 등록되지 않은 주소에 대해서는 "not a proxy contract"로 revert합니다.

이름을 string으로 쓰는 이유

컨트랙트 식별자는 uint256이나 bytes32가 아니라 string을 씁니다. 레지스트리는 프로토콜의 최상위 컨트랙트여서 사용자가 블록 탐색기나 DAO를 통해 직접 상호작용하는 일이 흔할 수 있으므로, 코드 가독성과 사람 실수 감소를 가스 비용보다 우선했기 때문입니다. string 식별자 때문에 이벤트 매개변수는 indexed로 만들지 않았습니다. 32바이트보다 긴 string indexed 매개변수는 이름의 keccak256 해시로 바뀌어 읽기 어려워지기 때문입니다. 이 제안은 현재 Last Call 상태(마감 2025-07-31)로, 최종 확정 전 마지막 의견 수렴 단계의 명세입니다.

쓸 때 주의할 점

ERC 원문 · ← ERCs