概述
????????現(xiàn)代軟件的設(shè)計(jì)原則是“敏捷開(kāi)發(fā),迅速迭代”,功能升級(jí)或bug修復(fù)是所有軟件系統(tǒng)都要面對(duì)的問(wèn)題。甚至可以說(shuō)軟件質(zhì)量在很大程度上依賴于升級(jí)和修補(bǔ)源代碼的能力。當(dāng)然Dapp(去中心化應(yīng)用)也不例外,尤其Dapp一切都是透明的,這使得任何級(jí)別的bug都會(huì)被成倍的放大,因此可升級(jí)的智能合約成為所有Dapp的必然選擇。
????????本文主要以openzeppelin為基礎(chǔ)來(lái)闡述構(gòu)建可升級(jí)智能合約的一般流程和注意事項(xiàng)。
原理
openzeppelin通過(guò)在用戶與智能合約中間加入一個(gè)代理來(lái)實(shí)現(xiàn)合約的透明升級(jí),用戶直接與代理交互,代理將用戶的請(qǐng)求轉(zhuǎn)發(fā)到實(shí)際合約,同時(shí)將合約的執(zhí)行結(jié)果響應(yīng)給用戶。
示意圖
如上圖所示,升級(jí)時(shí)只需要讓Proxy指向Implementation合約即可。
上圖有如下三種類型合約:
- Proxy
- ?用戶直接也該合約交互;
- 所有的狀態(tài)變量都在該合約中維護(hù);
- 該合約符合EIP1967標(biāo)準(zhǔn);
- 該合約將用戶請(qǐng)求透明的轉(zhuǎn)發(fā)到Implementation合約,同時(shí)將Implementation合約的返回響應(yīng)給用戶;
- Implementation
????????該合約被稱為邏輯合約,Dapp的所有邏輯都在該合約中完成,Proxy以delegatecall的形式調(diào)用該合約中的方法。
- ProxyAdmin
????????在介紹該合約前我們先考慮一個(gè)問(wèn)題——我們?nèi)绾握{(diào)用Proxy本身的方法?比如Proxy與Implementation都有一個(gè)方法upgradeTo(address),那么當(dāng)用戶調(diào)用該方法時(shí),Proxy是該調(diào)用其自身方法還是以delegatecall的形式調(diào)用Implementation?
????????OpenZeppelin是通過(guò)”透明代理“(transparent proxy )的模式來(lái)解決這個(gè)問(wèn)題的。該模式通過(guò)發(fā)起調(diào)用的地址來(lái)決定如何調(diào)用方法。
- 發(fā)起調(diào)用的地址為Proxy的管理地址(部署Proxy的地址)時(shí),Proxy將執(zhí)行自己的方法。
- 發(fā)起調(diào)用的地址為其它地址時(shí),Proxy將以delegatecall的形式向Implementation發(fā)起調(diào)用。
假設(shè)Proxy有owner()和upgradeTo()方法,Implementation有owner()和transfer()方法,則不同用戶發(fā)起調(diào)用時(shí)具體調(diào)用方法如下:
msg.sender | owner() | upgradeto() | transfer() |
---|---|---|---|
Owner |
returns proxy.owner() |
returns proxy.upgradeTo() |
fails |
Other |
returns Implementation.owner() |
fails |
returns Implementation.transfer() |
??????? 通過(guò)上面的討論我們可以看出,部署Proxy合約的賬戶無(wú)法調(diào)用Implementation合約中的方法,為此OpenZeppelin用ProxyAdmin來(lái)管理部署Proxy,此時(shí)Proxy的部署者為ProxyAdmin,這樣用戶就不用擔(dān)心本地賬戶無(wú)法調(diào)用Implementation的情況,當(dāng)然OpenZeppelin也提供了專門的接口用于更改Proxy的管理者。
執(zhí)行流程
- 用戶向Proxy發(fā)起調(diào)用。
- Proxy捕獲用戶請(qǐng)求,同時(shí)以delegatecall的形式向Implementation發(fā)起調(diào)用。此處理解delegatecall與call的區(qū)別尤其重要。
- Implementation收到請(qǐng)求后執(zhí)行相關(guān)邏輯,并將結(jié)果返回給Proxy。值得注意的是由于上步中以delegatecall形式調(diào)用,因此該合約中邏輯是在Proxy的上下文中執(zhí)行。
- Proxy將收到的返回?cái)?shù)據(jù)響應(yīng)給用戶。
示例
????????下面在hardhat本地節(jié)點(diǎn),以自動(dòng)售貨機(jī)的合約升級(jí)為例來(lái)說(shuō)明合約升級(jí)流程。
啟動(dòng)本地節(jié)點(diǎn)
npx hardhat node
合約版本1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
// import "hardhat/console.sol";
contract VendingMachineV1 is Initializable {
// these state variables and their values
// will be preserved forever, regardless of upgrading
uint public numSodas;
address public owner;
function initialize(uint _numSodas) public initializer {
numSodas = _numSodas;
owner = msg.sender;
}
function purchaseSoda() public payable {
require(msg.value >= 1000 wei, "You must pay 1000 wei for a soda!");
numSodas--;
}
function withdrawProfits() public onlyOwner {
require(
address(this).balance > 0,
"Profits must be greater than 0 in order to withdraw!"
);
(bool sent, ) = owner.call{value: address(this).balance}("");
require(sent, "Failed to send ether");
}
function setNewOwner(address _newOwner) public onlyOwner {
owner = _newOwner;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function.");
_;
}
}
部署腳本
const { ethers, upgrades } = require('hardhat');
async function main() {
const VendingMachineV1 = await ethers.getContractFactory('VendingMachineV1');
const proxy = await upgrades.deployProxy(VendingMachineV1, [100]);
await proxy.waitForDeployment();
const proxyAddr = await proxy.getAddress();
const implementationAddress = await upgrades.erc1967.getImplementationAddress(
proxyAddr
);
console.log('Proxy contract address: ' + proxyAddr);
console.log('Implementation contract address: ' + implementationAddress);
}
main();
?合約部署
執(zhí)行部署腳本
npx hardhat run scripts/deployProxy.js --network localhost
部署腳本執(zhí)行后會(huì)打印出代理地址和VendingMachineV1合約的地址,如下:
合約版本2
該版本較版本1添加了supplySoda方法用于補(bǔ)充庫(kù)存,為了縮短篇幅這處省略版本1中相同的代碼部分。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract VendingMachineV2 is Initializable {
//....
//....舊版本相同的代碼
function supplySoda(uint _num) public {
require(_num > 0);
numSodas += _num;
}
}
合約升級(jí)
???????? 為了更清晰的說(shuō)明升級(jí)過(guò)程,此處我們?cè)趆ardhat終端進(jìn)行升級(jí)。
- 開(kāi)啟hardhat終端
npx hardhat console --network localhost
- 在終端中我們先觀察版本1合約的當(dāng)前狀態(tài),如下圖:
從圖中我們可以看到,Proxy指向的Implementation的地址與前面部署版本1時(shí)輸出的地址一致。
注:此處箭頭1是Proxy的地址,箭頭2是Implementation的地址
- 部署版本2
由上圖可知,更新后Proxy已指向了新版本的地址(見(jiàn)紅框)。執(zhí)行新版本的方法補(bǔ)充庫(kù)存后結(jié)果如下:
要點(diǎn)
構(gòu)造函數(shù)
????????通過(guò)OpenZeppelin編寫(xiě)可升級(jí)合約時(shí),在合約中不能有構(gòu)造函數(shù),一般將初始化的操作放在一個(gè)initialize的普通函數(shù)中(當(dāng)然可以是任意的函數(shù)名,此時(shí)只需要調(diào)用部署API時(shí)指定該函數(shù)即可),升級(jí)過(guò)程中OpenZeppelin組件會(huì)主動(dòng)執(zhí)行該函數(shù)。
??????? 構(gòu)造函數(shù)與普通函數(shù)最大的區(qū)別是,構(gòu)造函數(shù)只在部署時(shí)執(zhí)行一次,而普通函數(shù)可以多次執(zhí)行,因此需要向initialize函數(shù)加上initializer修飾符,該modifer只允許該函數(shù)執(zhí)行一次。
??????? 在執(zhí)行上面升級(jí)版本2時(shí),我們發(fā)現(xiàn)更新版本時(shí)雖然我們新版本傳入了構(gòu)造函數(shù)的參數(shù)但新版本的initialize并未執(zhí)行。
upgrades.upgradeProxy('0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512', V2, {opts:{constructorArgs:[300]}});
添加變量
????????在編寫(xiě)合約的新版本時(shí),無(wú)論是由于新特性還是由于bug修復(fù),都有一個(gè)額外的限制需要遵守:您不能更改合約狀態(tài)變量聲明的順序,也不能更改它們的類型。具體原因看這里。
??????? 一般在寫(xiě)可升級(jí)合約時(shí)我們需要預(yù)留一些空間,以允許該合約的未來(lái)版本在不影響子合約的存儲(chǔ)布局的情況下使用這些槽。通用的做法是在基礎(chǔ)合約中預(yù)先定義固定大小的uint256數(shù)組(由于EVM以槽為單位執(zhí)行操作,而槽大小為32字節(jié)),該數(shù)組一般定義為_(kāi)_gap或以__gap_為前綴,以便OpenZeppelin升級(jí)可以識(shí)別該數(shù)組為預(yù)留空間,當(dāng)版本升級(jí)需要新加變量時(shí)可以釋放該數(shù)組的空間,如下:
//升級(jí)前合約
contract Base {
uint256 base1;
uint256[50] __gap;
}
contract Child is Base {
uint256 child;
}
//升級(jí)后合約
contract Base {
uint256 base1;
uint256 base2;
uint256[49] __gap;
}
或者
contract Base {
uint256 base1;
uint128 base2a;
uint128 base2b;
uint256[49] __gap;
}
Proxy透明轉(zhuǎn)發(fā)的原理
??????? 其實(shí)質(zhì)是在proxy的fallback函數(shù)中添加如下邏輯。
// This code is for "illustration" purposes. To implement this functionality in production it
// is recommended to use the `Proxy` contract from the `@openzeppelin/contracts` library.
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.2/contracts/proxy/Proxy.sol
assembly {
// (1) copy incoming call data
calldatacopy(0, 0, calldatasize())
// (2) forward call to logic contract
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
// (3) retrieve return data
returndatacopy(0, 0, returndatasize())
// (4) forward return data back to caller
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
Proxy與implementation狀態(tài)變量沖突
??????? 通過(guò)Unstructured Storage Proxies來(lái)解決
參考文檔:
Proxy Upgrade Pattern - OpenZeppelin Docs文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-802558.html
Writing Upgradeable Contracts - OpenZeppelin Docs文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-802558.html
到了這里,關(guān)于基于openzeppelin編寫(xiě)solidity可升級(jí)的智能合約的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!