使用OpenZeppelin编写可升级合约
为了方便社区新手入门,本文尽可能的详细
准备合约编译环境
测试系统:WSL ubuntu 20.04
node: v10.19.0
npm: 6.14.4
创建环境工程
mkdir myContract
cd myContract
sudo npm i -g truffle
truffle init
此时目录下会生成truffle工程相关文件
contracts migrations test truffle-config.js
编写测试合约
需要部署三个合约,分别是
- Parms(逻辑合约)
- ProxyAdmin(管理合约)
- TransparentUpgradeProxy(代理合约,DAPP直接交互的合约地址)
Params合约代码
在myContract/contract/Parms.sol
文件,并输入
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract Params is Initializable,OwnableUpgradeable {
function initialize()public initializer{
__Context_init_unchained();
__Ownable_init_unchained();
}
mapping(string => uint256) private uint256Params;
event Uint256ParamSetted(string indexed _key,uint256 _value);
function SetUint256Param(string memory _key,uint256 _value) external onlyOwner{
uint256Params[_key] = _value;
emit Uint256ParamSetted(_key,_value);
}
function GetUint256Param(string memory _key)public view returns(uint256){
return uint256Params[_key];
}
}
ProxyAdmin合约代码
在myContract/contract/ProxyAdmin.sol
文件,并输入
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an
* explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}.
*/
contract ProxyAdmin is Ownable {
/**
* @dev Returns the current implementation of `proxy`.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function getProxyImplementation(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("implementation()")) == 0x5c60da1b
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b");
require(success);
return abi.decode(returndata, (address));
}
/**
* @dev Returns the current admin of `proxy`.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("admin()")) == 0xf851a440
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440");
require(success);
return abi.decode(returndata, (address));
}
/**
* @dev Changes the admin of `proxy` to `newAdmin`.
*
* Requirements:
*
* - This contract must be the current admin of `proxy`.
*/
function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner {
proxy.changeAdmin(newAdmin);
}
/**
* @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function upgrade(TransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner {
proxy.upgradeTo(implementation);
}
/**
* @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. See
* {TransparentUpgradeableProxy-upgradeToAndCall}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
function upgradeAndCall(
TransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}
TransparentUpgradeableProxy 代理合约代码
在myContract/contract/TransparentUpgradeProxy.sol
文件,并输入
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
/**
* @dev This contract implements a proxy that is upgradeable by an admin.
*
* To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector
* clashing], which can potentially be used in an attack, this contract uses the
* https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two
* things that go hand in hand:
*
* 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if
* that call matches one of the admin functions exposed by the proxy itself.
* 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the
* implementation. If the admin tries to call a function on the implementation it will fail with an error that says
* "admin cannot fallback to proxy target".
*
* These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing
* the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due
* to sudden errors when trying to call a function from the proxy implementation.
*
* Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way,
* you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy.
*/
contract TransparentUpgradeableProxy is ERC1967Proxy {
/**
* @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and
* optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}.
*/
constructor(
address _logic,
address admin_,
bytes memory _data
) payable ERC1967Proxy(_logic, _data) {
assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1));
_changeAdmin(admin_);
}
/**
* @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin.
*/
modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}
/**
* @dev Returns the current admin.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the
* https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103`
*/
function admin() external ifAdmin returns (address admin_) {
admin_ = _getAdmin();
}
/**
* @dev Returns the current implementation.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the
* https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`
*/
function implementation() external ifAdmin returns (address implementation_) {
implementation_ = _implementation();
}
/**
* @dev Changes the admin of the proxy.
*
* Emits an {AdminChanged} event.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}.
*/
function changeAdmin(address newAdmin) external virtual ifAdmin {
_changeAdmin(newAdmin);
}
/**
* @dev Upgrade the implementation of the proxy.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}.
*/
function upgradeTo(address newImplementation) external ifAdmin {
_upgradeToAndCall(newImplementation, bytes(""), false);
}
/**
* @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified
* by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the
* proxied contract.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}.
*/
function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin {
_upgradeToAndCall(newImplementation, data, true);
}
/**
* @dev Returns the current admin.
*/
function _admin() internal view virtual returns (address) {
return _getAdmin();
}
/**
* @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}.
*/
function _beforeFallback() internal virtual override {
require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target");
super._beforeFallback();
}
}
修改truffle solc版本为0.8.0
打开truffle-config.js
,修改solc版本
compilers: {
solc: {
version: "^0.8.0"
...
编译合约
首次需要安装依赖
npm i --save @openzeppelin/contracts-upgradeable
npm i --save @openzeppelin/contracts
编译合约
truffle complite
部署合约
等待完成后,会在build\contracts
找到对应的编译后的合约
如何使用编译后的文件进行合约部署,参考《MyEtherWallet使用》
Params合约部署完的合约地址:0x808189EB5932Cfe710A849752CDb54F5fb1b85DA
ProxyAdmin合约部署完的合约地址:0x9b05f1F378B52011215464f4Ad4666BC80B7bcA5
部署TransparentUpgradeableProxy时,
_LOGIC:逻辑合约地址,这里为 0x808189EB5932Cfe710A849752CDb54F5fb1b85DA
ADMIN_:管理合约地址,这里为 0x9b05f1F378B52011215464f4Ad4666BC80B7bcA5
_DATA:逻辑合约初始化方法调用数据,这里为0x8129fc1c(只调用initialize方法,initialize方法没有入参,如果有参数也是支持的)
0x8129fc1c
由以下获得
web3js.eth.abi.encodeFunctionCall({
name: 'initialize',
type: 'function',
inputs: []
}, []);
>0x8129fc1c
部署后的地址是:0x95e19C3609DE02291840CE9093c75e233cF2Cf08
此时用MyEtherWallet执行合约地址:0x95e19C3609DE02291840CE9093c75e233cF2Cf08,使用Params合约的abi执行
调用SetUint256Param,设置_key=K,_VALUE=1,并通过GetUint256Param进行验证
此时完成了基本的通过TransparentUpgradeableProxy代理合约调用Params逻辑合约的过程。
下面开始演示通过ProxyAdmin合约将Params逻辑合约升级
升级Params逻辑合约
修改Params合约,并部署
function GetUint256Param(string memory _key)public view returns(uint256){
uint256 v = uint256Params[_key];
return v+1;
}
新部署后的地址为0xBf72f7C1e39B76D7A5b702E16251Fdd132Cd6618
调用ProxyAdmin进行升级
ProxyAdmin提供两个方法进行升级
- upgrade,需要传入proxy地址,新的逻辑实现地址
- upgradeAndCall,需要传入roxy地址,新的逻辑实现地址,初始化调用数据
本例中,由于数据是保存在代理合约中,这份数据已经初始化过了,不需要再初始化,所以调用upgrade方法即可,参数如下:
- proxy: 0x95e19C3609DE02291840CE9093c75e233cF2Cf08
- implementation: 0xBf72f7C1e39B76D7A5b702E16251Fdd132Cd6618
至此,合约升级完毕。调用GetUint256Param方法进行验证,获得_key=K的value为2,合约升级成功。
参考
https://docs.openzeppelin.com/contracts/3.x/api/proxy
https://www.cnblogs.com/cqvoip/p/15033402.html
https://docs.openzeppelin.com/upgrades-plugins/1.x/
https://medium.com/@kenschiller/making-ethereum-smart-contracts-upgradable-aa16a4256d32