你將會在這篇文章看到
- 甚麼是升級合約
- 可升級合約的執行過程
- 建立ERC20的可升級合約
什麼是可升級合約(Upgradable Contract)?
顧名思義,就是可以升級的合約。(被打)
一般來說,區塊鏈最令人耳熟能詳的就是不可竄改性,任何程式碼只要上鏈了就不能夠更改了,這賦予了區塊鏈最強大的功能,然後反面過來思考就是,萬一你個合約寫壞了,你也沒有辦法去更改,這不符合軟體產業快速迭代的特性了,可升級合約就是為了解決此問題,以下我們會介紹這跟一般合約有甚麼不同,接著會教學建立的步驟。
合約架構
可升級合約就是利用代理合約去實現升級的效果,如下圖所示,我們把一張合約分拆成 Proxy Contract
跟 Logic Contract
,將資料存在代理合約和程式邏輯儲存在邏輯合約中,所以升級的時候,舊有的資料並不會消失,而是會繼續保留在合約中,而抽象的邏輯就可以隨著升級的合約更新。
以上是最簡單的代理合約模型,你點進去來源網址會發現,實際上的代理合約模式是更複雜的。但在合約的架構上可以分為三種,當你第一次佈署代理合約的時候就會發現,共有三個合約被佈署,分別是代理合約管理員 Proxy Admin
、可升級代理合約 Upgradeability Proxy
、實例合約 Implementation Contract
,以下分別介紹:
- 實例合約
Implementation Contract
: 可被升級邏輯合約,可以藉由每次佈署不同的合約達到改變邏輯的效果,要注意的是變數等儲存資訊是不能被改動的,會導致合約崩潰。 - 代理合約管理員
Proxy Admin
: 儲存代理合約的擁有者,只有擁有者才能升級合約,並且在升級的時候呼叫Upgradeability Proxy
更新Implementation Contract
的地址。 - 可升級代理合約
Upgradeability Proxy
: 代理合約本人,地址永遠不變,所有使用者直接對該合約進行操作,會儲存Implementation Contract
的地址。
代理合約跟一般合約的不同點
solidity中的constructor 並不是 runtime bytecode 的一部分,只會在佈署的過程中運行一次,所以代理合約無法使用實例合約的constructor,因為已經在佈署時運行過了,因此我們把要把實例合約的的程式碼移到 initialize
function 中,如此就不會被solidity限制。
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;import "@openzeppelin/upgrades/contracts/Initializable.sol";contract MyContract is Initializable {
uint256 public x; function initialize(uint256 _x) public initializer {
x = _x;
}
}
還有一個不同的地方,Solidity會自動啟動其他父層合約的constructor,但在 initializer
的狀況中,你需要手動處理。
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;import "@openzeppelin/upgrades/contracts/Initializable.sol";contract BaseContract is Initializable {
uint256 public y; function initialize() public initializer {
y = 42;
}
}contract MyContract is BaseContract {
uint256 public x; function initialize(uint256 _x) public initializer {
BaseContract.initialize(); // Do not forget this call!
x = _x;
}
}
初始值跟constructor一樣只有deploy時有作用,因此要將值放在 initialize
中
// 正確
contract MyContract is Initializable {
uint256 public hasInitialValue; function initialize() public initializer {
hasInitialValue = 42; // set initial value in initializer
}
}//錯誤
contract MyContract {
uint256 public hasInitialValue = 42; function initialize() public initializer {
}
}
佈署過程
佈署代理合約的過程很繁瑣,所以我們採用 openzeppelin-upgrades 的外掛插件,這個外掛會把複雜的佈署一次處理完畢,以下來介紹這個外掛做了甚麼事情。
佈署合約時要使用 deployProxy
- 確認合約是安全的 (upgrade safe)
- 佈署實例合約
Implementation Contract
- 佈署代理合約管理員
Proxy Admin
- 初始化實例合約
Implementation Contract
- 佈署可升級代理合約
Upgradeability Proxy
注意: 以上步驟是我看完原始碼執行跟合約佈署狀態後理解的順序,但跟官方文件的順序不同,大家可以一起研究指正。
升級合約要使用upgradeProxy
- 取得
proxy admin
權限,必須要是管理員才能升級合約 - 確認合約是安全的 (upgrade safe )
- 確認實例合約是不是有被佈署過,沒有再進行佈署
- 佈署要升級的實例合約
- 呼叫
Proxy Admin
合約,更新代理合約上的實例合約地址
補充: 如果Implementation Contract
的程式碼沒有改變,但又佈署一次 proxy 的話,則 impl. contact不會再被deploy,僅會佈署proxy contract。
https://github.com/OpenZeppelin/openzeppelin-upgrades
佈署 ERC20 代理合約
接下來我們就開始運行我們的程式碼吧,環境使用hardhat。
安裝hardhat,選擇建立空的config
$ npm install --save-dev hardhat$ npx hardhat
Welcome to Hardhat v2.0.2
✔ What do you want to do? · Create an empty hardhat.config.js
Config file created
用hardhat建鏈 (我個人是習慣用ganache)
$ npx hardhat node
設定 hardhat.config.js
,根據你的網路設定調整,可參考文件
/**
* @type import('hardhat/config').HardhatUserConfig
*/
require('@nomiclabs/hardhat-ethers');
require('@openzeppelin/hardhat-upgrades');module.exports = {
defaultNetwork: "ganache",
networks: {
ganache: {
url: "http://172.17.144.1:7545",
// accounts: [privateKey1, privateKey2, ...]
}
},
solidity: {
version: "0.6.12",
},
};
建立合約
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.7.5;
import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";contract TestToken is Initializable, ERC20Upgradeable {
function initialize(string memory name_, string memory symbol_, uint256 initialSupply) public virtual initializer {
__ERC20_init(name_, symbol_);
_mint(msg.sender, initialSupply);
}
}
建立佈署合約程式碼
const { ethers, upgrades } = require("hardhat");async function main() {
const TestToken = await ethers.getContractFactory("TestToken");
const testToken = await upgrades.deployProxy(TestToken, ['TestToken', 'TST', 100000000000]);
await testToken.deployed();
console.log("testToken deployed to:", testToken.address);
} main();
佈署合約
npx hardhat run ./scripts/erc20-deploy-proxy.js
取得合約資訊,記得把地址改為生成的合約地址
const { BigNumber } = require("ethers");
const { ethers, upgrades } = require("hardhat");
async function main() {
const address = "0x8675Cfe9ef7815f43E08e87cda8438F5D7AAF5Fe";
const TestToken = await ethers.getContractFactory("TestToken");
const testToken = await TestToken.attach(address);
var totalSupply = await testToken.totalSupply();
console.log("testToken totalSupply:", totalSupply.toString());
const balances = ["0xF89fA5bC76F5C945FAb248bb50fDA846774a9BF9", "0xEd5aa8E471D012e18BeF2A35ADE4501d7Afe51c6", "0x2B2443067B14B989B488012cBb147b68EaC02891"];
balances.forEach((account, i) => {
var qqq = testToken.balanceOf(account).then(value => {
console.log("account", i, "balance: ", value.toString())
return value
});
});
} main()
.then()
.catch(error => {
console.error(error);
process.exit(1);
});
其他操作可以參考我的github : https://github.com/cfengliu/upgradable-contract
補充: 儲存的問題
- 實例合約的地址存在哪?
- 實例合約的變數存在哪?
2. 會存在代理合約上:
因為使用delegatecall的關係,代理合約 storage slot 會儲存變數的值,實例合約的變數會指到proxy合約的變數。
如果你喜歡這篇文章,請幫我拍拍手,你的支持是我繼續寫的動力。
參考文件
- https://github.com/OpenZeppelin/openzeppelin-upgrades
- https://blog.openzeppelin.com/the-state-of-smart-contract-upgrades
- https://blog.openzeppelin.com/proxy-patterns/
- https://simpleaswater.com/upgradable-smart-contracts/
- https://docs.openzeppelin.com/learn/upgrading-smart-contracts
- https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable
- https://zhuanlan.zhihu.com/p/55662982
- https://programtheblockchain.com/posts/2018/03/09/understanding-ethereum-smart-contract-storage/