🧠 UUPS vs Transparent Proxy 실전 비교


항목 UUPS Proxy Transparent Proxy
🧱 구조 Proxy가 매우 단순Logic이 업그레이드 로직 내장 Proxy가 업그레이드 기능 포함Logic은 기능만 담당
🛠 업그레이드 함수 위치 Logic(Implementation) 계약 안에 있음 Proxy(AdminProxy) 계약 안에 있음
📥 초기화 방식 initialize() initialize()
🧪 사용 난이도 약간 더 어려움 (보안 신경 써야 함) 더 간단하고 자동화됨
🔐 보안 실수로 Logic에 upgradeTo() 빼먹으면 영원히 못 업그레이드 Proxy가 모든 업그레이드를 처리하므로 비교적 안전
💰 가스비 ✅ 더 저렴함 (최대 30~40% 차이) ❌ 더 비쌈 (Proxy가 업그레이드 기능 포함)
🧩 사용 시점 대규모 프로젝트, 가스 절감이 중요한 경우 대부분의 일반적인 상황
🚀 OpenZeppelin CLI/Plugin 지원 ✅ 완벽 지원 (kind: "uups") ✅ 기본 지원

✅ 코드 구조 비교

🔷 Transparent Proxy (기본 방식)

Logic.sol

contract LaunchPad is Initializable, OwnableUpgradeable {
uint public value;

function initialize(uint _value) public initializer {
    __Ownable_init();
    value = _value;
}

function setValue(uint _value) external {
    value = _value;
}

}

배포

const LaunchPad = await ethers.getContractFactory("LaunchPad");
const proxy = await upgrades.deployProxy(LaunchPad, [42], {
initializer: "initialize"
});

업그레이드

const NewVersion = await ethers.getContractFactory("LaunchPadV2");
await upgrades.upgradeProxy(proxy.address, NewVersion);

🔷 UUPS Proxy (가스 최적화 방식)

Logic.sol

contract LaunchPad is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint public value;

function initialize(uint _value) public initializer {
    __Ownable_init();
    __UUPSUpgradeable_init();
    value = _value;
}

function _authorizeUpgrade(address newImpl) internal override onlyOwner {}

}

배포

const LaunchPad = await ethers.getContractFactory("LaunchPad");
const proxy = await upgrades.deployProxy(LaunchPad, [42], {
kind: "uups", // ✅ UUPS 지정
initializer: "initialize"
});

업그레이드

const NewVersion = await ethers.getContractFactory("LaunchPadV2");
await upgrades.upgradeProxy(proxy.address, NewVersion);

💰 가스비 실전 비교 예시

함수 Transparent Proxy UUPS Proxy
배포 시 200,000 gas 이상 150,000 gas 정도
상태 변경 함수 호출 60,000 gas 45,000 gas
upgradeProxy() 호출 Transparent가 더 간단함 Logic 쪽에 upgradeTo 구현 필수

차이는 클 수 있지만, 극단적일 때 기준입니다.

🔒 보안 실수 위험 예시 (UUPS)

// 만약 _authorizeUpgrade()를 누락하면?
// contract 업그레이드 불가능 상태로 영구 봉인됨 ❌

UUPS에서는 반드시 function _authorizeUpgrade()를 구현해야 하며,
잘못 구현하거나 안 넣으면 큰 사고로 이어질 수 있습니다.

🏁 정리: 어떤 걸 쓰면 좋을까?

상황 추천 방식
보안 우선, 복잡하지 않음 ✅ Transparent Proxy
가스 최적화가 중요 (NFT 대량 발행, 여러 컨트랙트 배포 등) ✅ UUPS Proxy
실험/연습/기초 학습 ✅ Transparent (단순함)
여러 컨트랙트를 동일한 로직으로 관리하고 싶다 ✅ Beacon Proxy 고려

✨ 결론

“처음에는 Transparent로 시작하고, 필요할 때 UUPS로 넘어가라”
→ OpenZeppelin도 이 전략을 기본 권장합니다.


이하는 Hardhat + TypeScript 환경에서 Transparent Proxy vs UUPS Proxy를 직접 배포하고 비교하는 예시입니다.

🧱 필요한 계약 파일 (요약)

🔹 LaunchPadTransparent.sol

pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract LaunchPadTransparent is Initializable, OwnableUpgradeable {
uint public value;

function initialize(uint _value) public initializer {
    __Ownable_init();
    value = _value;
}

function setValue(uint _value) external {
    value = _value;
}

}

🔹 LaunchPadUUPS.sol

pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract LaunchPadUUPS is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint public value;

function initialize(uint _value) public initializer {
    __Ownable_init();
    __UUPSUpgradeable_init();
    value = _value;
}

function setValue(uint _value) external {
    value = _value;
}

function _authorizeUpgrade(address) internal override onlyOwner {}

}

🚀 실행 방법
1. .sol 파일들을 contracts/ 폴더에 저장
2. 위 main() 스크립트를 scripts/test-uups-vs-transparent.ts로 저장
3. 아래 명령 실행

npx hardhat run scripts/test-uups-vs-transparent.ts --network localhost

+ Recent posts