ERC-1167 최소 프록시 컨트랙트 (Minimal Proxy Contract)
Final · 기타 · 2018-06-22 작성
같은 기능을 하는 컨트랙트를 아주 작은 바이트코드로 값싸게 복제하는 표준입니다. 복제본은 모든 호출을 정해진 원본 컨트랙트에 넘기고 그 결과를 그대로 돌려줍니다.
왜 필요한가
같은 기능의 컨트랙트가 여러 개 필요할 때 매번 전체 코드를 새로 배포하면 배포 비용이 그만큼 커집니다. 호출을 다른 주소로 넘기는 방식을 각자 다르게 만들면 지갑이나 블록 탐색기 같은 도구는 그 주소의 바이트코드를 보고도 실제로 어떤 코드가 실행되는지 알 수 없습니다. 그러면 복제본이 정말 원본과 같은 동작을 하는지 외부에서 확인하기 어렵습니다. 이 표준은 바이트코드 형태를 하나로 고정해서 누구나 복제본의 코드를 읽고 원본 주소를 바로 알아내게 합니다. 그래서 원본의 검증된 소스나 외부 감사 결과를 복제본의 동작에 대한 근거로 쓸 수 있습니다. 복제본은 호출 데이터와 가스 전부를 원본에 넘기고 반환값은 물론 실패했을 때의 메시지까지 그대로 전달합니다. 사용자는 원본을 직접 부른 것과 같은 결과를 받습니다.
심층 설명
표준 바이트코드
표준 복제 컨트랙트의 배포된 바이트코드는 363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 입니다. 이 중 10번째부터 29번째 바이트(양 끝 포함, 모두 20바이트)를 원본 기능 컨트랙트의 주소로 바꿉니다. 예시의 bebe로 채워진 자리가 그 위치입니다. 바이트코드가 한 가지로 고정되어 있어서 도구는 어떤 주소의 코드가 이 패턴과 맞는지 비교한 뒤 그 자리의 20바이트를 읽어 원본 주소를 알아낼 수 있습니다. 참조 구현은 optionality/clone-factory 저장소에 있습니다.
실행 순서 분해
배포된 코드는 네 단계로 움직입니다. 먼저 calldatasize와 calldatacopy로 들어온 호출 데이터 전체를 메모리 0번지부터 복사합니다. 다음으로 push20으로 원본 주소를 올리고 gas로 남은 가스 전부를 얹어 delegatecall을 실행합니다. 호출이 끝나면 returndatasize와 returndatacopy로 반환 데이터를 메모리로 복사합니다. 마지막으로 delegatecall의 성공 여부를 보고 push1 0x2b와 jumpi로 분기합니다. 성공이면 0x2b의 jumpdest로 건너뛰어 return 하고 실패면 바로 다음의 revert를 실행합니다. 두 경우 모두 반환 데이터를 그대로 넘기므로 원본이 메시지와 함께 revert 하면 그 메시지가 호출자에게 전달됩니다.
EIP-211 의존과 가스 절약
이 바이트코드는 한 호출 프레임 안에서 아직 아무 호출도 하지 않았을 때 returndatasize가 0을 돌려준다는 EIP-211 규칙에 기댑니다. 그래서 스택에 0을 올릴 자리마다 push나 dup 대신 returndatasize(0x3d)를 씁니다. 원문은 가스를 최대한 줄이려는 선택이라고 설명하며 returndatasize가 dup 계열보다 가스를 1 적게 쓴다고 밝힙니다. 이 의존 때문에 헤더의 requires 항목에 EIP-211이 올라 있습니다.
설계 목표
원문이 밝힌 목표는 여섯 가지입니다. 복제본 배포 가스가 싸야 합니다. 팩토리 컨트랙트 모델로 생성 트랜잭션 안에서 복제본 초기화를 지원해야 합니다. 바이트코드가 단순해서 도구가 바이트코드를 직접 읽고 판단하기 쉬워야 합니다(clone-factory의 CloneProbe.sol 참고). 동작이 고정되어 바뀌지 않아야 합니다. 이 표준은 업그레이드를 다루려고 만든 것이 아니고 더 강한 보장을 위해 일부러 그렇게 두었습니다. 실행 부담이 작아서 호출마다 call 한 번의 비용만 더해져야 합니다. 그리고 revert 메시지가 호출자에게 거슬러 올라가야 합니다.
배니티 주소 최적화
원본 컨트랙트를 앞쪽 바이트가 0으로 시작하는 주소에 배포하면 프록시 바이트코드를 더 줄일 수 있습니다. 주소 앞에 0 바이트가 Z개 있으면 push20 대신 pushN(N은 20에서 Z를 뺀 값)을 쓰고 0이 아닌 N바이트만 넣습니다. 이때 revert 분기용 점프 주소도 Z만큼 줄입니다. 원문 예시는 Z = 4인 경우로, push16으로 16바이트 주소를 올리고 점프 주소는 0x2b에서 0x27로 바뀝니다. 이렇게 하면 복제본 하나마다 바이트코드 4바이트를 아끼고 실행 시 가스에는 영향이 없습니다.
배포 코드, 테스트, 호환성
복제본을 만드는 배포 바이트코드는 이 명세에 포함되지 않습니다. 한 가지 방법이 참조 구현에 정의되어 있습니다. 테스트 케이스는 인수 없는 호출, 인수 있는 호출, 고정 길이 반환값, 가변 길이 반환값, revert 전달 확인 다섯 가지이고 참조 구현 프로젝트에 들어 있습니다. 호환성 문제는 없지만 이전 버전 프록시 바이트코드를 쓰는 시스템은 이 표준에 맞지 않습니다. 원문 상태는 Final입니다.