NamedBeacon와 Proxy: 업그레이드 가능한 솔리디티 계약 최적화 구현
개발자들은 일반적인 BeaconProxy에서 효율성 문제를 자주 겪습니다. 단 하나의 구현 주소만 저장하는 별도의 컨트랙트, 불필요하게 중복된 고정 및 스토리지 참조, 모든 프록시에 반복되는 관리 로직 등이 있습니다. 제안된 NamedBeacon와 NamedBeaconProxy 조합은 이러한 문제를 해결하여 가스 비용과 바이트코드 크기를 최소화합니다.
NamedBeacon는 문자열 형태의 bytes32 식별자를 사용해 구현 주소를 중앙 집중적으로 관리합니다. 프록시는 베이컨과 referenceId에 대한 고정 참조만 저장하며, 런타임에 현재 구현을 동적으로 가져옵니다.
NamedBeacon: 중앙 집중식 구현 등록소
이 컨트랙트는 mapping(bytes32 => address)를 통해 주소를 저장합니다. 핵심 기능 두 가지:
registerImplementation(bytes32 _referenceId, address _implementation)— 소유자만 접근 가능한 등록 기능.getImplementation(bytes32 _referenceId)— ID로 주소를 조회하며, 존재하지 않으면address(0)반환.
관리는 중앙집중화되어 있으며, registerImplementation 내부에서 소유자 확인이 강제됩니다. 구현 주소 검증을 통해 코드가 없는 0주소를 차단합니다. 오픈제플린과의 호환성을 위해 implementation() 함수를 포함했으며, 이는 address(this)를 반환합니다.
배포 후 고정 시나리오에서는 스토리지 사용이 이상적입니다. SlotStorage가 없어 코드가 간결하지만, 베이컨 업그레이드 기능은 제한됩니다.
전체 NamedBeacon 코드:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.33;
interface INamedBeacon {
function getImplementation(bytes32 _referenceId) external view returns (address _implementation);
function registerImplementation(bytes32 _referenceId, address _implementation) external;
}
contract NamedBeacon {
/// @dev 이 부분은 사용자 정의 인증 로직으로 재정의해야 합니다
address public owner;
/// @notice 주요 구현 주소 저장소
mapping(bytes32 implId => address implAddress) internal implementations;
error InvalidImplementation(address implementation);
error UnauthorizedAccess();
// 생성자
constructor (address _owner) {
/// @dev 이 부분은 사용자 정의 인증 로직으로 재정의해야 합니다
require (_owner != address(0));
owner = _owner;
}
/// @notice _referenceId에 등록된 구현 주소를 반환합니다. 해당 참조가 없으면 address(0) 반환.
/// @param _referenceId 참조된 컨트랙트의 고유 식별자
function getImplementation(bytes32 _referenceId) external view returns (address _implementation) {
return implementations[_referenceId];
}
/// @notice 지정된 _referenceId 아래에 _implementation을 등록합니다.
/// @param _referenceId 참조된 컨트랙트의 고유 식별자
/// @param _implementation 구현 주소
function registerImplementation(bytes32 _referenceId, address _implementation) external {
require (msg.sender == owner, UnauthorizedAccess());
if (_implementation != address(0) && _implementation.code.length == 0) {
revert InvalidImplementation(_implementation);
}
implementations[_referenceId] = _implementation;
}
/// @notice 오픈제플린 베이컨 기능을 모방하기 위한 특수 함수
/// @dev 오직 오픈제플린 베이컨 프록시와의 호환성을 위해 존재합니다
/// @dev BeaconProxy.ctor() => ERC1967Utils.upgradeBeaconToAndCall(beacon, data) => _setBeacon(address newBeacon) => address beaconImplementation = IBeacon(newBeacon).implementation();
/// @dev 1. 유효한 주소를 반환해야 함
/// @dev 2. 그리고 컨트랙트여야 함: if (beaconImplementation.code.length == 0) { revert }
/// @dev 비슷한 문제가 ERC1967Utils.upgradeBeaconToAndCall(address newBeacon, bytes memory data)의 줄에서도 발생함
/// @dev `Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);`
/// @dev 따라서,
function implementation() external view returns (address _current) {
return address(this);
}
}
NamedBeaconProxy: 최소한의 업그레이드 가능한 프록시
Proxy를 확장하고 오픈제플린의 Address를 사용합니다. 고정 필드인 beacon과 implementationReferenceId는 스토리지 슬롯을 사용하지 않습니다.
생성자에서 수행하는 작업:
- 베이컨 유효성 검사 (
code.length > 0). - referenceId를 통해 베이컨에서 구현을 가져와
code.length검사. - 선택적으로
_data로 초기화를 위한 delegatecall 실행.
재정의된 _implementation()은 INamedBeacon(beacon).getImplementation(implementationReferenceId)를 반환합니다.
receive()를 지원하며, msg.value > 0이고 _data가 없을 경우 NonPayable 에러를 발생시킵니다.
전체 NamedBeaconProxy 코드:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.33;
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { Proxy } from "@openzeppelin/contracts/proxy/Proxy.sol";
import { INamedBeacon } from "./NamedBeacon.sol";
contract NamedBeaconProxy is Proxy {
address private immutable beacon;
bytes32 private immutable implementationReferenceId;
error NonPayable();
error InvalidBeacon(address beacon);
error InvalidImplementation(address implementation);
error ImplementationNotFound(bytes32 implementationReferenceId);
constructor(address _beacon, bytes32 _implementationReferenceId, bytes memory _data) payable {
// 베이컨 설정
if (_beacon.code.length == 0) {
revert InvalidBeacon(_beacon);
}
beacon = _beacon;
// 구현 참조 ID 설정
address implementationFromBeacon = INamedBeacon(_beacon).getImplementation(_implementationReferenceId);
if (implementationFromBeacon.code.length == 0) {
revert InvalidImplementation(implementationFromBeacon);
}
implementationReferenceId = _implementationReferenceId;
// 데이터 제공 시 초기화
if (_data.length > 0) {
Address.functionDelegateCall(implementationFromBeacon, _data);
} else {
_checkNonPayable();
}
}
function _implementation() internal view virtual override returns (address) {
return INamedBeacon(beacon).getImplementation(implementationReferenceId);
}
receive() external payable {}
function _checkNonPayable() private {
if (msg.value > 0) {
revert NonPayable();
}
}
}
배포 및 사용 워크플로우
- 소유자와 함께 NamedBeacon 배포.
- 구현 컨트랙트 배포.
- 등록:
registerImplementation(keccak256('v1'), implementationAddress). - NamedBeaconProxy 배포(
beacon,referenceId,initData).
프록시 내의 검증 체크는 호출 시 가스 절약을 위해 비활성화할 수 있습니다. 소유자 로직은 파생된 NamedBeacon 컨트랙트에서 맞춤형으로 수정 가능합니다.
주요 장점
- bytes32 ID를 통한 구현 주소 중앙 집중 저장으로 코드 중복 감소.
- 프록시 내 고정 참조로 스토리지 슬롯 사용 제거.
- 최소한의 프록시 크기: 관리 로직 없이 베이컨으로의 delegatecall만 존재.
- 완전한 오픈제플린 호환성:
implementation()을 지원해upgradeBeaconToAndCall활용 가능. - 실무용 설계: 솔리디티 0.8.33, 하드햇 3.1.6, 포괄적인 테스트 커버리지 제공.
— Editorial Team
아직 댓글이 없습니다.