ERC1967Upgrade库实现了基于ERC1967标准(代理合约的slot分布)的slots读写函数,并在对应slot更新时emit出标准中相应的event。对于各种可升级合约和代理合约的实现而言,本库的作用举足轻重。
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
ERC1967Upgrade库实现了基于ERC1967标准(代理合约的slot分布)的slots读写函数,并在对应slot更新时emit出标准中相应的event。对于各种可升级合约和代理合约的实现而言,本库的作用举足轻重。
ERC1967详情参见:https://eips.ethereum.org/EIPS/eip-1967
继承ERC1967Upgrade合约:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
contract MockERC1967Upgrade is ERC1967Upgrade {
bytes32 private constant _ROLLBACK_SLOT = bytes32(uint(keccak256("eip1967.proxy.rollback")) - 1);
function setRollbackSlot(bool value) external {
StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value = value;
}
function getImplementation() external view returns (address){
return _getImplementation();
}
function upgradeTo(address newImplementation) external {
_upgradeTo(newImplementation);
}
function upgradeToAndCall(
address newImplementation,
bytes memory data,
bool forceCall
) external {
_upgradeToAndCall(newImplementation, data, forceCall);
}
function upgradeToAndCallUUPS(
address newImplementation,
bytes memory data,
bool forceCall
) external {
_upgradeToAndCallUUPS(newImplementation, data, forceCall);
}
function getAdmin() external view returns (address) {
return _getAdmin();
}
function changeAdmin(address newAdmin) external {
_changeAdmin(newAdmin);
}
function getBeacon() external view returns (address) {
return _getBeacon();
}
function upgradeBeaconToAndCall(
address newBeacon,
bytes memory data,
bool forceCall
) external {
_upgradeBeaconToAndCall(newBeacon, data, forceCall);
}
}
全部foundry测试合约:
测试使用的物料合约:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sol";
interface IImplement {
event InitialCallWithoutArgs();
event InitialCallWithArgs(uint, address, string);
event Receive();
event Fallback(bytes);
}
contract Implement is IImplement {
function initialCallWithoutArgs() external {
emit InitialCallWithoutArgs();
}
function initialCallWithArgs(uint arg1, address arg2, string memory arg3) external {
emit InitialCallWithArgs(arg1, arg2, arg3);
}
receive() external payable {
emit Receive();
}
fallback() external {
emit Fallback(msg.data);
}
}
contract ImplementERC1822Proxiable is Implement, IERC1822Proxiable {
bytes32 public proxiableUUID;
constructor(bytes32 newProxiableUUID){
proxiableUUID = newProxiableUUID;
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol";
contract Beacon is IBeacon {
address public implementation;
constructor(address newImplementation){
implementation = newImplementation;
}
}
_getImplementation() internal
:返回当前存储的逻辑合约地址;_upgradeTo(address newImplementation) internal
:将代理合约背后的逻辑合约地址升级更换为newImplementation。 // 用于存储逻辑合约地址的slot号
// 计算逻辑:keccak256("eip1967.proxy.implementation")-1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
function _getImplementation() internal view returns (address) {
// 读取编号为_IMPLEMENTATION_SLOT的slot中的值,并转为address类型
// 注:StorageSlot.getAddressSlot()详解参见:https://learnblockchain.cn/article/6104
return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
}
// 设置逻辑合约地址为newImplementation
function _setImplementation(address newImplementation) private {
// 检验新的逻辑合约地址是合约地址
// 注:Address.isContract()详解参见:https://learnblockchain.cn/article/6098
require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
// 将新的逻辑合约地址写入编号为_IMPLEMENTATION_SLOT的slot中
StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
}
function _upgradeTo(address newImplementation) internal {
// 设置新的逻辑合约地址
_setImplementation(newImplementation);
// 按照ERC1967标准,在升级逻辑合约地址后抛出事件`Upgraded(address indexed)`
emit Upgraded(newImplementation);
}
foundry代码验证:
contract ERC1967UpgradeTest is Test, IERC1967 {
MockERC1967Upgrade private _testing = new MockERC1967Upgrade();
Implement private _implement = new Implement();
function test_GetImplementationAndUpgradeTo() external {
assertEq(_testing.getImplementation(), address(0));
address newImplementationAddress = address(_implement);
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementationAddress);
_testing.upgradeTo(newImplementationAddress);
assertEq(_testing.getImplementation(), newImplementationAddress);
// revert if new implementation address is not a contract
vm.expectRevert("ERC1967: new implementation is not a contract");
_testing.upgradeTo(address(1024));
}
}
将代理合约背后的逻辑合约地址升级更换为newImplementation,再执行一个额外的到新逻辑合约的delegatecall。
注:这个delegatecall可以理解为是去调用一个适配合约升级的类似constructor的函数。
function _upgradeToAndCall(
address newImplementation,
bytes memory data,
bool forceCall
) internal {
// 将代理合约背后的逻辑合约地址升级更换为newImplementation
_upgradeTo(newImplementation);
if (data.length > 0 || forceCall) {
// 如果data不为空或forceCall为true,将delegatecall到新逻辑合约(以data作为calldata)
// 注:Address.functionDelegateCall()详解参见:https://learnblockchain.cn/article/6098
Address.functionDelegateCall(newImplementation, data);
}
}
foundry代码验证:
contract ERC1967UpgradeTest is Test, IERC1967, IImplement {
MockERC1967Upgrade private _testing = new MockERC1967Upgrade();
Implement private _implement = new Implement();
function test_UpgradeToAndCall() external {
assertEq(_testing.getImplementation(), address(0));
address newImplementationAddress = address(_implement);
// case 1: no call
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementationAddress);
_testing.upgradeToAndCall(newImplementationAddress, '', false);
assertEq(_testing.getImplementation(), newImplementationAddress);
// revert if new implementation address is not a contract
vm.expectRevert("ERC1967: new implementation is not a contract");
_testing.upgradeToAndCall(address(1024), '', false);
// case 2: call with no argument
newImplementationAddress = address(new Implement());
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementationAddress);
vm.expectEmit(address(_testing));
emit IImplement.InitialCallWithoutArgs();
bytes memory data = abi.encodeCall(_implement.initialCallWithoutArgs, ());
_testing.upgradeToAndCall(newImplementationAddress, data, false);
assertEq(_testing.getImplementation(), newImplementationAddress);
// revert if new implementation address is not a contract
vm.expectRevert("ERC1967: new implementation is not a contract");
_testing.upgradeToAndCall(address(1024), data, false);
// case 3: call with arguments
newImplementationAddress = address(new Implement());
uint arg1 = 1024;
address arg2 = address(1024);
string memory arg3 = "1024";
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementationAddress);
vm.expectEmit(address(_testing));
emit IImplement.InitialCallWithArgs(arg1, arg2, arg3);
data = abi.encodeCall(
_implement.initialCallWithArgs,
(arg1, arg2, arg3)
);
_testing.upgradeToAndCall(newImplementationAddress, data, false);
assertEq(_testing.getImplementation(), newImplementationAddress);
// revert if new implementation address is not a contract
vm.expectRevert("ERC1967: new implementation is not a contract");
_testing.upgradeToAndCall(address(1024), data, false);
// case 4: with forceCall and no data
// NOTE: force call to the receive function of Implement
newImplementationAddress = address(new Implement());
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementationAddress);
vm.expectEmit(address(_testing));
emit IImplement.Receive();
_testing.upgradeToAndCall(newImplementationAddress, '', true);
assertEq(_testing.getImplementation(), newImplementationAddress);
// revert if new implementation address is not a contract
vm.expectRevert("ERC1967: new implementation is not a contract");
_testing.upgradeToAndCall(address(1024), '', true);
// case 5: with forceCall and data
// NOTE: it will enter the fallback function of Implement with non-selector data
newImplementationAddress = address(new Implement());
data = 'unknown';
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementationAddress);
vm.expectEmit(address(_testing));
emit IImplement.Fallback(data);
_testing.upgradeToAndCall(newImplementationAddress, data, true);
assertEq(_testing.getImplementation(), newImplementationAddress);
// revert if new implementation address is not a contract
vm.expectRevert("ERC1967: new implementation is not a contract");
_testing.upgradeToAndCall(address(1024), data, true);
}
}
将代理合约背后的逻辑合约地址升级更换为newImplementation并对其进行UUPS安全检查,再执行一个额外的到新逻辑合约的delegatecall。
注:这个delegatecall可以理解为是去调用一个适配合约升级的类似constructor的函数。
// 用于存储rollback测试标志的slot号。
// 如果当前的升级调用_upgradeToAndCallUUPS()处于rollback测试中,应先将该slot中设置为非0值。反之该slot中设置为0值。
// 计算逻辑:keccak256("eip1967.proxy.rollback")-1
bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;
function _upgradeToAndCallUUPS(
address newImplementation,
bytes memory data,
bool forceCall
) internal {
// 注:将UUPS proxy背后的逻辑合约从旧升到新时一般需要进行一个rollback测试,来确保逻辑合约的兼容性。
// 该测试是将代理背后的新逻辑合约升级回旧逻辑合约,如果可以回滚成功就说明新逻辑合约是有效的UUPS逻辑合约。
if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) {
// 如果当前_upgradeToAndCallUUPS()的调用是处于UUPS的rollback测试中,
// 那么将逻辑合约地址直接设置为newImplementation
_setImplementation(newImplementation);
} else {
// 如果当前_upgradeToAndCallUUPS()的调用不是处于UUPS的rollback测试中,
// 那么将调用newImplementation合约的proxiableUUID方法,并检查返回值。
// 注:这是在检查新逻辑合约的有效兼容性
try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) {
// 如果成功获得newImplementation合约的proxiableUUID方法的返回值,要求该返回值为_IMPLEMENTATION_SLOT
// 否则认为newImplementation合约不兼容
require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID");
} catch {
// 如果newImplementation合约中没有proxiableUUID方法,说明新逻辑合约不是UUPS逻辑合约,直接revert
revert("ERC1967Upgrade: new implementation is not UUPS");
}
// 将proxy合约背后的逻辑合约地址升级更换为newImplementation,再执行一个额外的到新逻辑合约的delegatecall
_upgradeToAndCall(newImplementation, data, forceCall);
}
}
foundry代码验证:
contract ERC1967UpgradeTest is Test, IERC1967, IImplement {
MockERC1967Upgrade private _testing = new MockERC1967Upgrade();
Implement private _implement = new Implement();
function test_UpgradeToAndCallUUPS() external {
assertEq(_testing.getImplementation(), address(0));
address newImplementationAddress = address(_implement);
// case 1: in rollback test
// NOTE: only change implementation address no matter what the data and forceCall arguments are
_testing.setRollbackSlot(true);
_testing.upgradeToAndCallUUPS(newImplementationAddress, '', false);
assertEq(_testing.getImplementation(), newImplementationAddress);
newImplementationAddress = address(new Implement());
_testing.upgradeToAndCallUUPS(newImplementationAddress, '1024', false);
assertEq(_testing.getImplementation(), newImplementationAddress);
newImplementationAddress = address(new Implement());
_testing.upgradeToAndCallUUPS(newImplementationAddress, '', true);
assertEq(_testing.getImplementation(), newImplementationAddress);
newImplementationAddress = address(new Implement());
_testing.upgradeToAndCallUUPS(newImplementationAddress, '1024', true);
assertEq(_testing.getImplementation(), newImplementationAddress);
// case 2: out of rollback test
_testing.setRollbackSlot(false);
// case 2.1: with supported proxiableUUID
bytes32 proxiableUUID = bytes32(uint(keccak256("eip1967.proxy.implementation")) - 1);
// case 2.1.1: no call
address payable newImplementERC1822ProxiableAddress = payable(address(new ImplementERC1822Proxiable(proxiableUUID)));
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementERC1822ProxiableAddress);
_testing.upgradeToAndCallUUPS(newImplementERC1822ProxiableAddress, '', false);
assertEq(_testing.getImplementation(), newImplementERC1822ProxiableAddress);
// case 2.1.2: call with no argument
newImplementERC1822ProxiableAddress = payable(address(new ImplementERC1822Proxiable(proxiableUUID)));
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementERC1822ProxiableAddress);
vm.expectEmit(address(_testing));
emit IImplement.InitialCallWithoutArgs();
bytes memory data = abi.encodeCall(
ImplementERC1822Proxiable(newImplementERC1822ProxiableAddress).initialCallWithoutArgs,
()
);
_testing.upgradeToAndCallUUPS(newImplementERC1822ProxiableAddress, data, false);
assertEq(_testing.getImplementation(), newImplementERC1822ProxiableAddress);
// case 2.1.3: call with arguments
newImplementERC1822ProxiableAddress = payable(address(new ImplementERC1822Proxiable(proxiableUUID)));
uint arg1 = 1024;
address arg2 = address(1024);
string memory arg3 = "1024";
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementERC1822ProxiableAddress);
vm.expectEmit(address(_testing));
emit IImplement.InitialCallWithArgs(arg1, arg2, arg3);
data = abi.encodeCall(
ImplementERC1822Proxiable(newImplementERC1822ProxiableAddress).initialCallWithArgs,
(arg1, arg2, arg3)
);
_testing.upgradeToAndCallUUPS(newImplementERC1822ProxiableAddress, data, false);
assertEq(_testing.getImplementation(), newImplementERC1822ProxiableAddress);
// case 2.1.4: with forceCall and no data
// NOTE: force call to the receive function of Implement
newImplementERC1822ProxiableAddress = payable(address(new ImplementERC1822Proxiable(proxiableUUID)));
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementERC1822ProxiableAddress);
vm.expectEmit(address(_testing));
emit IImplement.Receive();
_testing.upgradeToAndCallUUPS(newImplementERC1822ProxiableAddress, '', true);
assertEq(_testing.getImplementation(), newImplementERC1822ProxiableAddress);
// case 2.1.5: with forceCall and data
// NOTE: it will enter the fallback function of Implement with non-selector data
newImplementERC1822ProxiableAddress = payable(address(new ImplementERC1822Proxiable(proxiableUUID)));
data = 'unknown';
vm.expectEmit(address(_testing));
emit IERC1967.Upgraded(newImplementERC1822ProxiableAddress);
vm.expectEmit(address(_testing));
emit IImplement.Fallback(data);
_testing.upgradeToAndCallUUPS(newImplementERC1822ProxiableAddress, data, true);
assertEq(_testing.getImplementation(), newImplementERC1822ProxiableAddress);
// case 2.2: revert with unsupported proxiableUUID
proxiableUUID = bytes32(uint(keccak256("eip1967.proxy.implementation")) - 2);
newImplementERC1822ProxiableAddress = payable(address(new ImplementERC1822Proxiable(proxiableUUID)));
vm.expectRevert("ERC1967Upgrade: unsupported proxiableUUID");
_testing.upgradeToAndCallUUPS(newImplementERC1822ProxiableAddress, '', false);
// case 2.3: revert if the new implementation was a non-ERC1822 compliant
proxiableUUID = bytes32(uint(keccak256("eip1967.proxy.implementation")) - 1);
newImplementERC1822ProxiableAddress = payable(address(new Implement()));
vm.expectRevert("ERC1967Upgrade: new implementation is not UUPS");
_testing.upgradeToAndCallUUPS(newImplementERC1822ProxiableAddress, '', false);
// case 2.4: revert without msg if the new implementation address is not a contract
vm.expectRevert();
_testing.upgradeToAndCallUUPS(address(1024), '', false);
}
}
_getAdmin() internal
:返回当前admin地址;_changeAdmin(address newAdmin) internal
:更换admin地址为newAdmin。 // 用于存储admin地址的slot号
// 计算逻辑:keccak256("eip1967.proxy.admin")-1
bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
function _getAdmin() internal view returns (address) {
// 读取编号为_ADMIN_SLOT的slot中的值,并转为address类型
// 注:StorageSlot.getAddressSlot()详解参见:https://learnblockchain.cn/article/6104
return StorageSlot.getAddressSlot(_ADMIN_SLOT).value;
}
function _changeAdmin(address newAdmin) internal {
// 按照ERC1967标准,在更换admin地址后抛出事件`AdminChanged(address,address)`
emit AdminChanged(_getAdmin(), newAdmin);
// 设置admin地址为newAdmin
_setAdmin(newAdmin);
}
// 设置admin地址为newAdmin
function _setAdmin(address newAdmin) private {
// 要求newAdmin不为0地址
require(newAdmin != address(0), "ERC1967: new admin is the zero address");
// 将newAdmin写入编号为_ADMIN_SLOT的slot中
StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
}
foundry代码验证:
contract ERC1967UpgradeTest is Test, IERC1967 {
MockERC1967Upgrade private _testing = new MockERC1967Upgrade();
function test_GetAdminAndChangeAdmin() external {
address currentAdmin = address(0);
assertEq(_testing.getAdmin(), currentAdmin);
address newAdminAddress = address(1024);
vm.expectEmit(address(_testing));
emit IERC1967.AdminChanged(currentAdmin, newAdminAddress);
_testing.changeAdmin(newAdminAddress);
assertEq(_testing.getAdmin(), newAdminAddress);
}
}
_getBeacon() internal
:返回当前存储的信标合约地址;_upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal
:将代理合约背后的信标合约地址升级更换为newBeacon,再执行一个额外的到新信标合约背后的逻辑合约的delegatecall。注:这个delegatecall可以理解为是去调用一个适配合约升级的类似constructor的函数。 // 用于存储信标合约地址的slot号
// 计算逻辑:keccak256("eip1967.proxy.beacon")-1
bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
function _getBeacon() internal view returns (address) {
// 读取编号为_BEACON_SLOT的slot中的值,并转为address类型
// 注:StorageSlot.getAddressSlot()详解参见:https://learnblockchain.cn/article/6104
return StorageSlot.getAddressSlot(_BEACON_SLOT).value;
}
function _upgradeBeaconToAndCall(
address newBeacon,
bytes memory data,
bool forceCall
) internal {
// 设置信标合约地址为newBeacon
_setBeacon(newBeacon);
// 按照ERC1967标准,在升级信标合约地址后抛出事件`BeaconUpgraded(address indexed)`
emit BeaconUpgraded(newBeacon);
if (data.length > 0 || forceCall) {
// 如果data不为空或forceCall为true,将delegatecall到新信标合约背后的逻辑合约(以data作为calldata)
// 注:Address.functionDelegateCall()详解参见:https://learnblockchain.cn/article/6098
Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);
}
}
// 设置信标合约地址为newBeacon
function _setBeacon(address newBeacon) private {
// 要求newBeacon是一个合约地址
// 注:Address.isContract()详解参见:https://learnblockchain.cn/article/6098
require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract");
// 要求newBeacon合约中存储的逻辑合约地址是一个合约地址
require(
Address.isContract(IBeacon(newBeacon).implementation()),
"ERC1967: beacon implementation is not a contract"
);
// 将newBeacon写入编号为_BEACON_SLOT的slot中
StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon;
}
foundry代码验证:
contract ERC1967UpgradeTest is Test, IERC1967, IImplement {
MockERC1967Upgrade private _testing = new MockERC1967Upgrade();
Implement private _implement = new Implement();
function test_GetBeaconAndUpgradeBeaconToAndCall() external {
assertEq(_testing.getBeacon(), address(0));
// case 1: no call
address newBeaconAddress = address(new Beacon(address(_implement)));
vm.expectEmit(address(_testing));
emit IERC1967.BeaconUpgraded(newBeaconAddress);
_testing.upgradeBeaconToAndCall(newBeaconAddress, '', false);
assertEq(_testing.getBeacon(), newBeaconAddress);
// case 2: call with no argument
newBeaconAddress = address(new Beacon(address(_implement)));
vm.expectEmit(address(_testing));
emit IERC1967.BeaconUpgraded(newBeaconAddress);
vm.expectEmit(address(_testing));
emit IImplement.InitialCallWithoutArgs();
bytes memory data = abi.encodeCall(
_implement.initialCallWithoutArgs,
()
);
_testing.upgradeBeaconToAndCall(newBeaconAddress, data, false);
assertEq(_testing.getBeacon(), newBeaconAddress);
// case 3: call with arguments
newBeaconAddress = address(new Beacon(address(_implement)));
uint arg1 = 1024;
address arg2 = address(1024);
string memory arg3 = "1024";
vm.expectEmit(address(_testing));
emit IERC1967.BeaconUpgraded(newBeaconAddress);
vm.expectEmit(address(_testing));
emit IImplement.InitialCallWithArgs(arg1, arg2, arg3);
data = abi.encodeCall(
_implement.initialCallWithArgs,
(arg1, arg2, arg3)
);
_testing.upgradeBeaconToAndCall(newBeaconAddress, data, false);
assertEq(_testing.getBeacon(), newBeaconAddress);
// case 4: with forceCall and no data
// NOTE: force call to the receive function of Implement
newBeaconAddress = address(new Beacon(address(_implement)));
vm.expectEmit(address(_testing));
emit IERC1967.BeaconUpgraded(newBeaconAddress);
vm.expectEmit(address(_testing));
emit IImplement.Receive();
_testing.upgradeBeaconToAndCall(newBeaconAddress, '', true);
assertEq(_testing.getBeacon(), newBeaconAddress);
// case 5: with forceCall and data
// NOTE: it will enter the fallback function of Implement with non-selector data
newBeaconAddress = address(new Beacon(address(_implement)));
data = 'unknown';
vm.expectEmit(address(_testing));
emit IERC1967.BeaconUpgraded(newBeaconAddress);
vm.expectEmit(address(_testing));
emit IImplement.Fallback(data);
_testing.upgradeBeaconToAndCall(newBeaconAddress, data, true);
assertEq(_testing.getBeacon(), newBeaconAddress);
// revert if new beacon address is not a contract
vm.expectRevert("ERC1967: new beacon is not a contract");
_testing.upgradeBeaconToAndCall(address(1024), '', false);
// revert if the implementation address in the new beacon is not a contract
newBeaconAddress = address(new Beacon(address(1024)));
vm.expectRevert("ERC1967: beacon implementation is not a contract");
_testing.upgradeBeaconToAndCall(newBeaconAddress, '', false);
}
}
ps: 本人热爱图灵,热爱中本聪,热爱V神。 以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。 同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下! 如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!