์ ๊ทธ๋ ์ด๋๋ธ(Upgradable) ์ปจํธ๋ํธ๋ ํ๋ก์ ํจํด์ ๊ธฐ๋ฐํ๋ค. ํ๋ก์ ํจํด์ด๋ ํด๋ผ์ด์ธํธ์ ์ค๋งํธ ์ปจํธ๋ํธ ์ค๊ฐ ๊ณ์ธต์ผ๋ก, ํด๋ผ์ด์ธํธ๊ฐ ์ค๋งํธ ์ปจํธ๋ํธ์ ์ง์ ์ ๊ทผํ๋ ๊ฒ์ด ์๋๋ผ ํ๋ก์ ์ปจํธ๋ํธ๋ฅผ ํตํด ๊ฐ์ ์ ์ผ๋ก ์ ๊ทผํ๋๋ก ํ๋ ํจํด์ด๋ค. ์ด๋ฐ ํ๋ก์ ํจํด์ fallback() ํจ์์ delegate_call() ํจ์, ์ด ๋ ๊ฐ์ง ํจ์์ ๋ฉ์ปค๋์ฆ์ด ํจ๊ป ์ฌ์ฉ๋์ด ๊ตฌํ๋๋ค.
ํ๋ก์ ํจํด ์ฐธ๊ณ : https://blog.openzeppelin.com/proxy-patterns/
Solidity์์ fallback ํจ์๊ฐ ํธ์ถ๋๋ ์กฐ๊ฑด์ ๋ค์๊ณผ ๊ฐ๋ค.
- EOA ๋๋ CA์์ ํน์ ์ปจํธ๋ํธ๋ก ์ด๋๋ฅผ ์ ์กํ๋ ๊ฒฝ์ฐ
- ์ปจํธ๋ํธ์ ์กด์ฌํ์ง ์๋ ํจ์๋ฅผ ํธ์ถํ ๋
- ์ปจํธ๋ํธ๊ฐ ๋ค๋ฅธ ์ปจํธ๋ํธ๋ก๋ถํฐ ๋ธ๋ฆฌ๊ฒ์ดํธ ์ฝ์ ๋ฐ๋ ๊ฒฝ์ฐ
์ต์ด์ ํธ์ถํ๋ ๊ณ์ ์ ์ค๊ณ ์ญํ ์ํ๋ ํ๋ก์ ์ปจํธ๋ํธ์ ์กด์ฌํ์ง ์๋ ํจ์๋ฅผ ํธ์ถํ์ฌ fallback() ํจ์๋ฅผ ๋ฐ๋์ํค๊ฒ๋๊ณ ์ด fallback() ํจ์ ๋ด๋ถ์์๋ delegate_call()์ ์ฌ์ฉํ์ฌ ์ต์ด ํธ์ถ ๊ณ์ ์ผ๋ก๋ถํฐ ์ ๋ฌ๋ ํ์ ์๊ทธ์ ๋ฐ์ดํฐ๋ฅผ ๋ก์ง์ ๋ด๋นํ๋ ์ํ๋ฆฌ๋ฉํธ ์ปจํธ๋ํธ์ ์ ๋ฌํ๋ค.
๋ธ๋ฆฌ๊ฒ์ดํธ ์ฝ์ ํธ์ถ๋ ์ปจํธ๋ํธ์ ์ฝ๋๋ฅผ ํธ์ถ์์ ์ปจํ ์คํธ์์ ์คํํ๋ ๋ฉ์ปค๋์ฆ์ด๋ค. ํ๋ก์ ์ปจํธ๋ํธ๋ฅผ ํตํด ์ํ๋ฆฌ๋ฉํธ(๋ก์ง) ์ปจํธ๋ํธ์ ํจ์๋ฅผ ์คํํ ๋, ํ๋ก์ ์ปจํธ๋ํธ์ ์ปจํ ์คํธ์์ ์ฝ๋๊ฐ ์คํ๋๋ฏ๋ก ํ๋ก์ ์ปจํธ๋ํธ์ ์ํ๊ฐ ๋ณ๊ฒฝ๋๋ค. ํ๋ก์ ์ปจํธ๋ํธ์ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋๋ ์๋ฆฌ์ด๋ค.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/proxy/Proxy.sol";
contract ERCProxy is Proxy {
address public implementation;
constructor(address _impleAddress, CampaignParams memory _campaignParams) {
implementation = _impleAddress;
}
function _implementation() internal view override returns (address) {
return implementation;
}
}
- ํ๋ก์ ์ปจํธ๋ํธ๋ฅผ ์์๋ฐ์ ํ
- ์์ฑ์์์ ์ํ๋ฆฌ๋ฉํธ ์ปจํธ๋ํธ ์ฃผ์๋ฅผ ์ ๋ฌํ์ฌ ์ ์ฅํ๋ค.
- ๋จ์ํ ์ํ ์ปจํธ๋ํธ ์ฃผ์๋ฅผ ๋ฆฌํดํ๋ _implementation()๋ฅผ ๊ตฌํํด์ค๋ค.
- ์ด์ Proxy ์ปจํธ๋ํธ๋ฅผ ์์๋ฐ์ ERCProxy ์ปจํธ๋ํธ์ ์ํ ์ปจํธ๋ํธ์ ์กด์ฌํ๋ ํจ์๋ฅผ ๋ฐ์ดํฐ(์ธ์๋ค)์ ํจ๊ป ํธ์ถํด์ฃผ๋ฉด Proxy ์ปจํธ๋ํธ์ fallback() ํจ์๋ฅผ ํ๊ณ ์ํ ์ปจํธ๋ํธ์ ํน์ ํจ์๋ฅผ ๋ธ๋ฆฌ๊ฒ์ดํธ ์ฝ ํ๊ฒ๋๋ค.
- ๋ง์ฝ ์ํ ์ปจํธ๋ํธ์ ์์ ์ฌํญ์ด ์๊ธด๋ค๋ฉด ์๋ก์ด ์ํ ์ปจํธ๋ํธ๋ฅผ ์ฌ ์์ฑ/๋ฐฐํฌ ํ implementation ๋ณ์๋ฅผ ์๋ก์ด ์ํ ์ปจํธ๋ํธ ์ฃผ์๋ก ์ ๋ฐ์ดํธํ๋ค.
ํ๋ก์ ํจํด์์๋ ํ๋ก์ ์ปจํธ๋ํธ๊ฐ ๋ก์ง ์ปจํธ๋ํธ์ ์์ฑ์๋ฅผ ํธ์ถํ ์ ์๋ค. ๋ก์ง ์ปจํธ๋ํธ ๋ฐฐํฌ์์ ์์ฑ์๊ฐ ๋ฑ ํ ๋ฒ๋ง ํธ์ถ๋๊ณ ์ด ์์ฑ์๋ ๋ค๋ฅธ ์ปจํธ๋ํธ์ ์ํด ํธ์ถ๋ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
๋ก์ง ์ปจํธ๋ํธ๊ฐ ๋ฐฐํฌ๋๊ฑฐ๋ ์ ๊ทธ๋ ์ด๋๋ ์ํฉ์์ ํ๋ก์ ์ปจํธ๋ํธ๋ ๋ก์ง ์ปจํธ๋ํธ์ ์ํ ๊ฐ, ์๋ฅผ๋ค์ด ๋ก์ง ์ปจํธ๋ํธ์ ์ฃผ์ ์ ์ฅ๊ณผ ๊ฐ์ ์ํ๋ณ์ ์ด๊ธฐํ ๋๋ ๋ณ๊ฒฝ์ด ํ์ํ๋ฐ ์์ฑ์๋ฅผ ์ฌ์ฉํ ์ ์์ผ๋ ์์ฑ์์ ์ ์ฌํ ๋ผ์ดํ์ฌ์ดํด์์ ๋จ ํ๋ฒ๋ง ํธ์ถ๋๋๋ก ์ค๊ณ๋ initialize() ํจ์๋ฅผ ๊ณ ์ํ๊ฒ ๋์๋ค.
์ฐธ๊ณ ๋ก ํ๋ก์์์ ๋ก์ง ์ปจํธ๋ํธ์ initialize() ํจ์๋ฅผ ํตํด ๋ก์ง ์ปจํธ๋ํธ์ ์ฃผ์๋ฅผ ์ํ ๋ณ์์ ์ ์ฅํ๋ค๋ฉด ๊ทธ๊ฒ์ ๋ธ๋ฆฌ๊ฒ์ดํธ์ฝ์ ์ํ์ฌ ํ๋ก์ ์ปจํธ๋ํธ์ ์ปจํ ์คํธ์ด๊ธฐ ๋๋ฌธ์ ์ ์ฅ์ ํ๋ก์ ์ปจํธ๋ํธ์ ์ ์ฅ๋๋ค.
openzeppelin Upgrades ํ๋ฌ๊ทธ์ธ์ ์ ๊ทธ๋ ์ด๋๋ธ ์ปจํธ๋ํธ๋ฅผ ์ฝ๊ฒ ๋ฐฐํฌ, ์ ๊ทธ๋ ์ด๋, ๊ด๋ฆฌ์ ๊ถํ ๊ด๋ฆฌ, ํ ์คํธํ ์ ์๋๋ก ๋๋๋ค. ๊ฐ๋ฐ์๊ฐ ํ๋ก์ ์ปจํธ๋ํธ๋ฅผ ๋ฐ๋ก ์์ฑํ ํ์๊ฐ ์๊ณ ๋ฐฐํฌ์, ์ ๊ทธ๋ ์ด๋, ๊ด๋ฆฌ์ ๊ถํ ๋ณ๊ฒฝ ๊ธฐ๋ฅ์ ํจ์๋ก ์ ๊ณตํ๋ค.
upgrades ํ๋ฌ๊ทธ์ธ์ UUPS, transparent, ๋น์ฝ ํ๋ก์ ํจํด์ ์ง์ํ๋ค.
- UUPS: ๋ก์ง ์ปจํธ๋ํธ๊ฐ ์์ ์ ์ ๊ทธ๋ ์ด๋ํ ์ ์๋ ๊ธฐ๋ฅ์ ๋ด์ฅํ๊ณ ์๋ค. ๊ฐ์ฅ ํํ ์ฌ์ฉ๋๋ฉฐ ์คํ์ ํ๋ฆฐ์์๋ UUPS ํ๋ก์ ํจํด ์ฌ์ฉ์ ๊ฐ์ฅ ๊ถ์ฅํ๋ค.
- Transparent: ๋ก์ง ์ปจํธ๋ํธ ์ฃผ์๋ฅผ ์ ๊ทธ๋ ์ด๋ํ๋ ํจ์๋ ํ๋ก์, ๋ก์ง ๋ ์ปจํธ๋ํธ์ ์กด์ฌํ๋ ์ฌ์ฉ์ ์ด์นด์ดํธ์ ์ด๋๋ฏผ ์ด์นด์ดํธ์ ํจ์ ํธ์ถ ๋์ ์ปจํธ๋ํธ๋ฅผ ๋ค๋ฅด๊ฒ ํ๋ค. (์ด๋๋ฏผ ๊ณ์ ์ ํ๋ก์ ์ปจํธ๋ํธ๋ก, ์ฌ์ฉ์ ๊ณ์ ์ ๋ก์ง ์ปจํธ๋ํธ๋ก) ์ด๋ก์จ ํจ์ ์ถฉ๋ ์ด์๋ฅผ ํด์ํ๋ค.
- Beacon:
์ฌ๋ฌ ํ๋ก์ ์ปจํธ๋ํธ๊ฐ ํ๋์ ๋น์ฝ ์ปจํธ๋ํธ๋ก๋ถํฐ ๋ก์ง ์ปจํธ๋ํธ์ ์ฃผ์๋ฅผ ์ป๋ ๋ฐฉ์์ผ๋ก, ๋ชจ๋ ํ๋ก์๋ ์ด ๋น์ฝ ์ปจํธ๋ํธ๋ฅผ ํตํด ์
๊ทธ๋ ์ด๋๋ ๋ก์ง ์ปจํธ๋ํธ ์ฃผ์๋ฅผ ์ป์ด ์ ๊ทผ.
UUPS ๋ฐ transparent ํ๋ก์์ ๊ฒฝ์ฐ deployProxy ๋ฐ upgradeProxy ํจ์๋ฅผ ์ฌ์ฉํ๊ณ , ๋น์ฝ ํ๋ก์์ ๊ฒฝ์ฐ deployBeacon, deployBeaconProxy ๋ฐ upgradeBeacon ํจ์๋ฅผ ์ฌ์ฉํ๋ค.
import { ethers, upgrades } from "hardhat";
async function main() {
const Box = await ethers.getContractFactory("Box");
const proxyInstance = await upgrades.deployProxy(Box, [42], {initializer: 'store'});
await proxyInstance.deployed();
console.log("Box Proxy deployed to:", proxyInstance.address);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
npx hardhat run scripts/create-box.ts --network {}
Box Proxy deployed to: 0x0F20c143a98CdfB3a1487278Bcd310296c674498
import { ethers, upgrades } from "hardhat";
const proxyAddress = "0x8d33046c43808974d76C2874c8BbA8eDc06EF495";
async function upgrade() {
const BoxV2 = await ethers.getContractFactory("BoxV2");
console.log("Preparing upgrade...");
// upgrades.prepareUpgrade() ํจ์๋ ์๋ก์ด ์ํ ์ปจํธ๋ํธ ์ฃผ์๋ฅผ ๋ฐํ
const boxV2Address = await upgrades.prepareUpgrade(proxyAddress, BoxV2);
console.log("BoxV2 Implemantaion address will be :", boxV2Address);
const boxV2Proxy = await upgrades.upgradeProxy(proxyAddress, BoxV2);
console.log("upgraded to same proxy address : ", boxV2Proxy.address);
}
upgrade();
npx hardhat run scripts/upgrade-box.ts --network {}
BoxV2 Implemantaion address will be : 0x4C18BB1a60fb0b9fF5747658cEC416CB91a9AE43
upgraded to same proxy address : 0x0F20c143a98CdfB3a1487278Bcd310296c674498
Impelmantion ์ปจํธ๋ ํธ๋ ์๋ก์ด v2 ์ปจํธ๋ํธ ์ฃผ์๋ก ๋ฐฐํฌ๋๊ณ , ์ ์ ์ ์ํธ์์ฉํ๋ ํ๋ก์ ์ฃผ์๋ ๋์ผํ ์ฃผ์๋ก์ ๊ทธ๋ ์ด๋ ๋๋ค.
- ๋ก์ง ์ปจํธ๋ํธ๋ฅผ ๋ฐฐํฌํ๊ณ
- ๋ก์ง ์ปจํธ๋ํธ์ ๊ธฐ๋ฅ์ ํ ์คํธ
import { expect } from 'chai';
import { Contract } from 'ethers';
import { ethers } from 'hardhat';
// contracts
let box: Contract;
// Start test block
describe('Box', function () {
beforeEach(async function () {
const Box = await ethers.getContractFactory("Box");
box = await Box.deploy();
await box.deployed();
console.log("box deployed",box.address);
});
// Test case
it('retrieve returns a value previously stored', async function () {
// Store a value
await box.store(42);
// Test if the returned value is the same one
// Note that we need to use strings to compare the 256 bit integers
expect((await box.retrieve()).toString()).to.equal('42');
});
});
- ๋ก์ง ์ปจํธ๋ํธ๋ฅผ ๋ฐฐํฌ
- ํ๋ก์ ์ปจํธ๋ํธ ๋ฐฐํฌ & ๋ก์ง ์ปจํธ๋ํธ์ ๋ฐ์ธ๋ฉ (์ด๊ธฐ ์คํ ํจ์ ์ง์ , ์ธ์ ๊ฐ ์ค์ )
- ํ๋ก์ ์ปจํธ๋ํธ ์ฃผ์๋ก๋ถํฐ ๋ก์ง ์ปจํธ๋ํธ์ ์ฃผ์ ๊ตฌํ๊ธฐ
upgrades.erc1967.getImplementationAddress(boxProxy.address); - ํ๋ก์ ์ปจํธ๋ํธ๋ฅผ ํตํด ์ด๊ธฐํ ์์ผฐ๋ ๋ฐ์ดํฐ๊ฐ ์ ์ ์ถ๋ ฅ ๋๋์ง ํ์ธ
import { expect } from "chai";
import { Contract } from "ethers";
import { ethers, upgrades } from "hardhat";
import { getImplementationAddress } from "@openzeppelin/upgrades-core";
// contracts
let boxProxy: Contract;
// Start test block
describe("Box (proxy)", function () {
beforeEach(async function () {
const Box = await ethers.getContractFactory("Box");
boxProxy = await upgrades.deployProxy(Box, [42], { initializer: "store" });
console.log("boxProxy deployProxy", boxProxy.address);
const currentImplAddress = await upgrades.erc1967.getImplementationAddress(
boxProxy.address
);
console.log("currentImplAddress", currentImplAddress);
});
// Test case
it("retrieve returns a value previously initialized", async function () {
// Test if the returned value is the same one
// Note that we need to use strings to compare the 256 bit integers
expect((await boxProxy.retrieve()).toString()).to.equal("42");
});
});
- ์๋ก์ด ๋ก์ง ์ปจํธ๋ํธ(BoxV2) ๋ฐฐํฌ
- ๋ก์ง ์ปจํธ๋ํธ ๊ธฐ๋ฅ ํ ์คํธ
import { expect } from 'chai';
import { Contract } from 'ethers';
import { ethers, upgrades } from 'hardhat';
// contracts
let boxV2: Contract;
// Start test block
describe('BoxV2', function () {
beforeEach(async function () {
const BoxV2 = await ethers.getContractFactory("BoxV2");
boxV2 = await BoxV2.deploy();
await boxV2.deployed();
console.log("boxV2 deployed",boxV2.address);
});
// Test case
it('retrieve returns a value previously stored', async function () {
// Store a value
await boxV2.store(42);
// Test if the returned value is the same one
// Note that we need to use strings to compare the 256 bit integers
expect((await boxV2.retrieve()).toString()).to.equal('42');
});
// Test case
it('retrieve returns a value previously incremented', async function () {
// Increment
await boxV2.increment();
// Test if the returned value is the same one
// Note that we need to use strings to compare the 256 bit integers
expect((await boxV2.retrieve()).toString()).to.equal('1');
});
});
- ๋ก์งV1 ์ปจํธ๋ํธ ๋ฐฐํฌ & ๋ก์งV1 ํ๋ก์ ๋ฐฐํฌ
- ๋ก์งV2 ์ปจํธ๋ํธ ๋ฐฐํฌ & ๋ก์งV2๋ก ์ ๊ทธ๋ ์ด๋
- ํ๋ก์๋ฅผ ํตํด ์ ๊ทธ๋ ์ด๋ ์ ์ ๋์ ํ์ธ
import { expect } from "chai";
import { Contract } from "ethers";
import { ethers, upgrades } from "hardhat";
// contracts
let boxProxy: Contract;
let boxV2Proxy: Contract;
// Start test block
describe("BoxV2 (proxy)", function () {
beforeEach(async function () {
const Box = await ethers.getContractFactory("Box");
const BoxV2 = await ethers.getContractFactory("BoxV2");
boxProxy = await upgrades.deployProxy(Box, [42], { initializer: "store" });
console.log("deployProxy", boxProxy.address);
let boxV2Implentaion = await upgrades.prepareUpgrade(
boxProxy.address,
BoxV2
);
console.log("implementation V2 address : ", boxV2Implentaion);
boxV2Proxy = await upgrades.upgradeProxy(boxProxy.address, BoxV2);
console.log("upgradeProxy", boxV2Proxy.address);
});
// Test case
it("retrieve returns a value previously incremented", async function () {
// Increment
await boxV2Proxy.increment();
// Test if the returned value is the same one
// Note that we need to use strings to compare the 256 bit integers
expect((await boxV2Proxy.retrieve()).toString()).to.equal("43");
});
});
์๋ก์ด ๊ธฐ๋ฅ์ด๋ ๋ฒ๊ทธ ์์ ์ผ๋ก ์ธํด ์ปจํธ๋ํธ์ ์ ๋ฒ์ ์ ์์ฑํ ๋, ์ค์ํด์ผํ ์คํ ๋ฆฌ์ง ๋ ์ด์์ ์ ํ ์ฌํญ์ด ์๋ค. ์ ๋ฐ์ ์ผ๋ก ์ปจํธ๋ํธ ์ํ ๋ณ์์ ์ ์ธ ์์์ ํ์ ์ ๋ณ๊ฒฝํ ์ ์๋ ๋ด์ฉ๋ค์ด๋ค.
์ด๋ฐ ์คํ ๋ฆฌ์ง ๋ ์ด์์ ์ ํ์ ์๋ฐํ๋ฉด ์ ๊ทธ๋ ์ด๋๋ ์ปจํธ๋ํธ์ ๊ธฐ์กด ์คํ ๋ฆฌ์ง์์ ์คํ ๋ฆฌ์ง ์ถฉ๋(Storage Collision)์ด ๋ฐ์ํ์ฌ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ๊ฐํ ์ค๋ฅ๊ฐ ๋ฐ์๋ ์ ์์ผ๋ฏ๋ก ๊ฐ๋ณํ ์ฃผ์๊ฐ ํ์ํ๋ค.
์ค์ ๋ก ์ ๊ทธ๋ ์ด๋๋ธ ์ปจํธ๋ํธ๋ฅผ ์์ฑํ ๋๋ ๊ธฐ์กด์ ๋ก์ง ์ปจํธ๋ํธ๋ฅผ ์์ํด์ ์์ฑํ์ฌ ๊ธฐ์กด์ ๋ณ์ ์ ์ธ์ ๋ณํ๊ฐ ์๋๋ก ํ๋๊ฒ์ด ์ผ๋ฐ์ ์ด๋ค.
https://forum.openzeppelin.com/t/korean-writing-upgradeable-contracts/2007
