투명 프록시 패턴은 프록시를 통해 로직 컨트랙트에 접근하는 방식이나. 업그레이드 관리 로직이 프록시 컨트랙트에 포함되어 있다. (관리자만이 컨트랙트를 업그레이드할 수 있도록 함)
로직 컨트랙트 주소를 업그레이드하는 함수는 프록시, 로직 두 컨트랙트에 존재하나 사용자 어카운트와 어드민 어카운트의 함수 호출 대상 컨트랙트를 다르게 함으로써 함수 충돌 이슈를 해소한다.
어드민 계정은 프록시 컨트랙트로, 사용자 계정은 로직 컨트랙트를 호출하도록 되어있다.
업그레이드 프로세스가 단순하고 이해하기 쉬움.
사용자와 개발자 모두 같은 주소를 사용하여 컨트랙트와 상호작용.
더 많은 가스 비용이 발생할 수 있음.
프록시와 구현체 간의 명확한 구분이 필요.
사용자와 개발자 모두가 동일한 인터페이스를 사용해야 하는 경우.
업그레이드 관리를 중앙에서 통제하고자 할 때.
가스 비용 최소화가 중요한 프로젝트.
업그레이드 프로세스에 더 많은 유연성이 필요한 경우.
관리자 주소의 보안이 매우 중요함.
업그레이드 시 데이터 호환성을 확인해야 함.
// Transparent Proxy 패턴 예시 코드
pragma solidity ^0.8.0;
contract LogicContract {
uint256 public data;
function setData(uint256 _data) external {
data = _data;
}
}
contract TransparentProxy {
address public logicContract;
address public owner;
constructor(address _logicContract) {
logicContract = _logicContract;
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_;
}
function _beforeFallback() internal virtual override {
require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target");
super._beforeFallback();
}
// fallback 함수를 사용하여 로직 컨트랙트의 함수를 호출
fallback() external payable {
address target = logicContract;
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), target, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
// 로직 컨트랙트를 업그레이드하는 함수
function upgradeLogic(address _newLogicContract) external onlyOwner {
logicContract = _newLogicContract;
}
}
TransparentProxy 컨트랙트는 로직 컨트랙트와 사용자 간의 인터페이스 역할. 사용자는 TransparentProxy를 통해 로직 컨트랙트의 함수를 호출할 수 있으며, TransparentProxy는 호출된 함수를 로직 컨트랙트로 전달. 사용자는 로직 컨트랙트를 직접 호출하는 것처럼 투명하게(transparently) 함수를 호출할 수 있음. 또한, TransparentProxy 컨트랙트의 소유자만이 로직 컨트랙트를 업그레이드할 수 있도록 onlyOwner modifier가 적용.
비콘 프록시 패턴은 여러 프록시 컨트랙트가 하나의 비콘 컨트랙트를 참조하여 로직 컨트랙트의 주소를 얻는 방식. 비콘은 로직 컨트랙트의 주소를 저장하고, 모든 프록시는 이 비콘을 통해 업그레이드된 로직에 접근.
여러 프록시 컨트랙트의 업그레이드를 중앙에서 관리할 수 있음.
업그레이드 시 가스 비용이 절약됨.
비콘 자체의 보안과 관리가 중요함.
모든 프록시가 동시에 업그레이드되어야 함.
동일한 로직을 사용하는 다수의 프록시 컨트랙트가 있는 경우.
중앙에서 여러 컨트랙트의 업그레이드를 효율적으로 관리하고자 할 때.
각 프록시 컨트랙트가 서로 다른 로직을 요구하는 경우.
업그레이드 과정에서 개별 컨트랙트의 특수한 처리가 필요한 경우.
비콘의 보안과 관리에 주의해야 함.
모든 프록시 컨트랙트가 동일한 업그레이드를 받게 되므로 주의 깊게 관리 필요.
다수의 NFT 컨트랙트가 동일한 로직을 공유하는 경우.
// Beacon contract - 프록시와 원본 컨트랙트 간의 상호작용을 관리
contract Beacon {
address public implementation;
// 프록시의 로직 컨트랙트를 설정하는 함수
function setImplementation(address _implementation) external {
implementation = _implementation;
}
}
// Logic contract - 실제 로직을 포함하는 스마트 컨트랙트
contract LogicContract {
uint256 public data;
// 상태를 변경하는 함수
function setData(uint256 _data) external {
data = _data;
}
// 상태를 조회하는 함수
function getData() external view returns (uint256) {
return data;
}
}
// BeaconProxy contract - 사용자와 프록시 사이의 인터페이스 역할
contract BeaconProxy {
address public beacon;
constructor(address _beacon) {
beacon = _beacon;
}
fallback() external {
address _impl = Beacon(beacon).implementation();
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let success := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
let retSz := returndatasize()
returndatacopy(ptr, 0, retSz)
switch success
case 0 {
revert(ptr, retSz)
}
default {
return(ptr, retSz)
}
}
}
}
Beacon 컨트랙트는 프록시와 로직 컨트랙트 간의 상호작용을 관리, 프록시 컨트랙트는 Beacon을 통해 실제 로직이 구현된 컨트랙트로 주소를 가져와 로직 컨트랙트에 델리게이트 콜
UUPS 패턴은 업그레이드 로직을 구현 컨트랙트 자체에 포함시키는 방식으로 현재 프록시 패턴에서 가장 흔히 쓰이는 패턴이다. (구현 컨트랙트가 자신을 업그레이드할 수 있는 기능을 내장하고 있음) 이는 프록시 컨트랙트가 업그레이드 로직을 가지지 않게 하여, 구현 컨트랙트에서만 업그레이드를 관리. 프록시 컨트랙트는 단순히 구현 컨트랙트로의 요청을 전달하는 역할만 수행한다. 오픈제플린에서도 Transparent가 아닌 UUPS 패턴 사용을 권장하고 있다.
구현 컨트랙트에서 업그레이드를 관리하기 때문에, 가스 비용이 절약됨.
업그레이드 로직과 구현 로직의 분리가 불필요.
구현 컨트랙트에 추가적인 복잡성이 생김.
잘못된 업그레이드가 시스템 전체에 영향을 줄 수 있음.
업그레이드 프로세스를 보다 유연하게 관리하고 싶은 경우.
가스 비용 절약이 중요한 프로젝트.
구현 컨트랙트의 복잡성을 최소화하고 싶은 경우.
업그레이드 로직을 별도로 관리하고자 하는 경우.
구현 컨트랙트의 업그레이드 로직에 대한 충분한 테스트가 필요함.
잘못된 업그레이드가 전체 시스템에 큰 영향을 줄 수 있으므로 주의 필요.
가스 비용 최적화가 중요한 대규모 애플리케이션.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 필요한 OpenZeppelin 컨트랙트를 가져옵니다.
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
// 구현 컨트랙트
contract MyContractV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
uint256 private value;
// 초기화 함수
function initialize(uint256 _value) public initializer {
__Ownable_init();
__UUPSUpgradeable_init();
value = _value;
}
// 업그레이드 권한 체크
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
// 비즈니스 로직 예시: 값 설정
function setValue(uint256 _value) public {
value = _value;
}
// 비즈니스 로직 예시: 값 조회
function getValue() public view returns (uint256) {
return value;
}
}
프록시 컨트랙트 배포와 업그레이드는 OpenZeppelin 플러그인을 사용.
프록시 컨트랙트는 OpenZeppelin의 플러그인을 사용하여 배포. deployProxy 함수를 사용하여 프록시와 초기 구현 컨트랙트를 배포, 초기화.
const { deployProxy } = require('@openzeppelin/hardhat-upgrades');
async function main() {
const MyContractV1 = await ethers.getContractFactory("MyContractV1");
const myContract = await deployProxy(MyContractV1, [42], {initializer: 'initialize'});
console.log("MyContractV1 deployed to:", myContract.address);
}
main().then(() => process.exit(0)).catch(error => {
console.error(error);
process.exit(1);
});
MyContractV1을 초기 구현 컨트랙트로 사용하여 프록시를 배포하고, 초기화 매개변수로 42를 전달. 배포 후, 프록시 컨트랙트 주소가 콘솔에 출력됨.
업그레이드
const { ethers, upgrades } = require("hardhat");
async function main() {
const MyContractV2 = await ethers.getContractFactory("MyContractV2");
const myContract = await upgrades.upgradeProxy(proxyAddress, MyContractV2);
console.log("MyContract upgraded to V2 at:", myContract.address);
}
main().then(() => process.exit(0)).catch((error) => {
console.error(error);
process.exit(1);
});
다이아몬드 패턴은 여러 기능을 갖는 스마트 컨트랙트를 하나의 주소(다이아몬드)에 결합할 수 있도록 설계된 패턴. 이를 통해 스마트 컨트랙트의 기능을 개별적으로 추가, 제거, 업데이트할 수 있다.
Facet 등록: diamondCut 함수를 호출하여 새로운 facet(기능 구현 컨트랙트)을 다이아몬드에 추가하거나, 기존 facet의 기능을 수정하거나 제거. 이 함수 호출은 주로 다이아몬드의 소유자나 관리자에 의해 수행된다. 어떤 facet 주소에서 어떤 함수 선택자(function selectors)를 추가, 교체, 또는 제거할지는 FacetCut 구조체를 통해 이뤄진다.
함수 선택자 매핑: 다이아몬드 패턴에서는 함수 선택자(함수의 시그니처를 나타내는 4바이트 해시)를 기반으로 요청을 적절한 facet으로 라우팅합니다. diamondCut을 통해 facet이 추가되면, 다이아몬드는 내부적으로 함수 선택자와 해당 facet 주소 간의 매핑을 저장합니다. 이 매핑 정보는 함수 호출이 어떤 facet으로 전달되어야 하는지 결정하는 데 사용됩니다.
사용자 호출 처리: 사용자가 다이아몬드 컨트랙트의 함수를 호출하면, 다이아몬드 컨트랙트의 fallback 함수 또는 receive 함수가 실행. 호출된 함수 선택자를 확인하고, 내부적으로 저장된 매핑 정보를 사용하여 해당 함수 선택자에 매핑된 facet으로(컨트랙트 주소) 요청을 전달. delegatecall을 통해 facet의 코드가 다이아몬드 컨트랙트의 컨텍스트(상태 변수 등)에서 실행된다.
함수 실행: 요청이 적절한 facet으로 라우팅되면, facet 내의 해당 함수가 실행된다. 이때, 함수 실행 결과는 다이아몬드 컨트랙트의 상태에 영향을 미침. 함수 실행이 완료되면, 결과(반환 값 또는 이벤트)는 사용자에게 전달된다.
복잡한 게임 또는 금융 서비스 스마트 컨트랙트.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IDiamondCut {
enum FacetAction { Add, Replace, Remove }
struct FacetCut {
address facetAddress;
FacetAction action;
bytes4[] functionSelectors;
}
function diamondCut(FacetCut[] calldata _facetCuts) external;
}
contract DiamondStorage {
struct FacetAddressAndSelector {
mapping(bytes4 => address) selectorToFacet;
}
function diamondStorage() internal pure returns (FacetAddressAndSelector storage ds) {
bytes32 position = keccak256("diamond.standard.diamond.storage");
assembly {
ds.slot := position
}
}
}
contract Diamond is IDiamondCut, DiamondStorage {
function diamondCut(FacetCut[] calldata _facetCuts) external override {
FacetAddressAndSelector storage ds = diamondStorage();
for (uint256 i = 0; i < _facetCuts.length; i++) {
FacetCut memory _facetCut = _facetCuts[i];
address _facetAddress = _facetCut.facetAddress;
for (uint256 j = 0; j < _facetCut.functionSelectors.length; j++) {
bytes4 selector = _facetCut.functionSelectors[j];
if (_facetCut.action == FacetAction.Add || _facetCut.action == FacetAction.Replace) {
ds.selectorToFacet[selector] = _facetAddress;
} else if (_facetCut.action == FacetAction.Remove) {
ds.selectorToFacet[selector] = address(0);
}
}
}
}
fallback() external payable {
FacetAddressAndSelector storage ds = diamondStorage();
address facet = ds.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()) }
}
}
}
Facet 추가, 교체, 제거: diamondCut 함수는 다이아몬드에 facet을 추가, 교체, 또는 제거하는 로직을 구현합니다. 각 facet에 대한 함수 선택자와 주소 매핑은 FacetAddressAndSelector 구조체를 통해 저장.
함수 호출 라우팅: fallback 함수는 사용자의 모든 호출을 처리하고, 저장된 매핑 정보를 사용하여 적절한 facet으로 요청을 라우팅. 이때 delegatecall을 사용하여 facet의 함수가 다이아몬드 컨트랙트의 상태와 컨텍스트에서 실행되도록 한다.
저장소 접근: diamondStorage 함수는 다이아몬드의 상태를 저장하기 위한 고유한 저장소 슬롯을 제공합. 이는 EIP-1967과 유사한 패턴을 사용하여 충돌을 방지.
이 예시는 다이아몬드 패턴의 기본적인 구현 방법을 보여준다. 실제 사용 시에는 권한 관리(예: diamondCut 함수에 대한 접근 제어), 에러 처리, 그리고 최적화를 포함한 추가적인 기능이 필요할 수 있다. EIP-2535 참고
미니멀 프록시 패턴은 매우 경량화된 구조로 설계되었으며, 주로 코드 재사용성을 높이고 가스 비용을 절감하기 위한 목적으로 사용. 이 패턴에서 프록시 컨트랙트는 단지 다른 주소(구현 컨트랙트)로의 호출을 위임(delegatecall)하는 역할만 수행. 각 미니멀 프록시 인스턴스는 동일한 구현(로직) 컨트랙트의 코드를 사용하여 작동하지만, 각각의 인스턴스는 독립적인 상태를 유지할 수 있다.
각 인스턴스의 독립적인 업그레이드가 필요한 경우. (로직 컨트랙트 주소가 생성자에서 초기화되어 수정 불가)
복잡한 상태 관리 및 로직:
업그레이드 유연성 부족: 미니멀 프록시 패턴은 구현 컨트랙트의 로직을 업그레이드하는 메커니즘을 내장하고 있지 않다. 구현 컨트랙트에 수정 사항이 발생하면, 새로운 구현 컨트랙트를 배포하고, 프록시 컨트랙트(또는 이를 생성하는 팩토리 컨트랙트)를 수정하여 새로운 구현 컨트랙트 주소를 사용하도록 해야 한다. 이 과정은 간단하지 않으며, 각 프록시 인스턴스의 독립적인 업그레이드를 지원하지 않는다.
복잡한 로직의 관리: 복잡한 상태 관리가 필요하거나, 다양한 비즈니스 로직을 포함하는 애플리케이션의 경우, 구현 컨트랙트의 변경이 빈번하게 발생할 수 있다. 미니멀 프록시 패턴은 이러한 변경을 간편하게 관리하고 반영하는 구조를 제공하지 않기 때문에, 복잡한 애플리케이션에서는 관리가 어려울 수 있다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Implementation {
uint public number;
function setNumber(uint _number) public {
number = _number;
}
function getNumber() public view returns (uint) {
return number;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MinimalProxy {
address internal _implementation;
constructor(address implementationAddress) {
_implementation = implementationAddress;
}
fallback() external payable {
address _impl = _implementation;
assembly {
// calldata를 구현 컨트랙트로 전달
calldatacopy(0, 0, calldatasize())
// 구현 컨트랙트로 delegatecall 실행
let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
// 반환 데이터를 복사
returndatacopy(0, 0, returndatasize())
// delegatecall이 성공적인지 체크 후 적절한 조치 취하기
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
Eternal Storage 패턴은 데이터를 유지하면서 스마트 컨트랙트의 로직을 업그레이드할 수 있도록 설계된 데이터 저장 방법. 데이터와 로직을 분리하여, 데이터 호환성을 보장.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EternalStorage {
mapping(bytes32 => uint256) internal uintStorage;
mapping(bytes32 => address) internal addressStorage;
// 데이터를 설정하는 함수
function setUint(bytes32 key, uint256 value) external {
uintStorage[key] = value;
}
function setAddress(bytes32 key, address value) external {
addressStorage[key] = value;
}
// 데이터를 가져오는 함수
function getUint(bytes32 key) external view returns (uint256) {
return uintStorage[key];
}
function getAddress(bytes32 key) external view returns (address) {
return addressStorage[key];
}
}
스토리지 컨트랙트는 다양한 타입의 데이터(예: uint, address, bytes, 등)를 저장할 수 있는 범용적인 키-값 저장소 형태를 취한다. 이 저장소는 데이터를 저장하고 검색하는 기능만을 제공하며, 이 데이터를 어떻게 사용할지에 대한 로직은 로직 컨트랙트에서 정의한다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IEternalStorage {
function setUint(bytes32 key, uint256 value) external;
function getUint(bytes32 key) external view returns (uint256);
}
contract LogicContract {
IEternalStorage public eternalStorage;
constructor(address _eternalStorage) {
eternalStorage = IEternalStorage(_eternalStorage);
}
// 데이터를 저장하는 예시 함수
function saveNumber(uint256 _number) public {
bytes32 key = keccak256(abi.encodePacked("number"));
eternalStorage.setUint(key, _number);
}
// 저장된 데이터를 가져오는 예시 함수
function getNumber() public view returns (uint256) {
bytes32 key = keccak256(abi.encodePacked("number"));
return eternalStorage.getUint(key);
}
}
사용자가 로직 컨트랙트가 변경되어도 주소 변환 없이 사용할 수 있도록 하려면, 이터널 스토리지 패턴을 프록시 패턴이나 다른 업그레이드 가능한 컨트랙트 구조와 함께 사용해야 한다.
간단한 프록시 패턴(Simple Proxy Pattern)
업그레이드 가능한 프록시 패턴(Upgradeable Proxy Pattern)
콜렉션 프록시 패턴(Collection Proxy Pattern)
권한 관리 프록시 패턴(Authorization Proxy Pattern)
게이트웨이 프록시 패턴(Gateway Proxy Pattern)
대체 프록시 패턴(Substitute Proxy Pattern)
멀티시그니처 프록시 패턴(Multisignature Proxy Pattern)
마스터-스레이브 프록시 패턴(Master-Slave Proxy Pattern)
보호 프록시 패턴(Protection Proxy Pattern)
자기수리 프록시 패턴(Self-repairing Proxy Pattern)
레이지 로딩 프록시 패턴(Lazy Loading Proxy Pattern)