前面的文章我们基本把合约升级的原理介绍完了,这篇文章我们来实际操作一下,部署一个可升级合约。我们将会使用到 hardhat 框架和 OpenZeppelin 的可升级合约库。这个库和 OZ 的普通合约库的区别是,所有的合约中都没有构造函数,作为代替的是 initialize 函数,用来作初始化操作。
首先执行下面的命令来做一些初始化的工作:
mkdir upgradeable_demo && cd upgradeable_demo
npm init -y
npm install --save-dev hardhat
npx hardhat,创建实例项目,并且按照步骤进行
npm install --save-dev @openzeppelin/hardhat-upgrades,安装 hardhat 的升级组件
npm install --save-dev @nomiclabs/hardhat-ethers ethers,这两个包主要用于合约部署和测试
npm install --save @openzeppelin/contracts-upgradeable,安装可升级合约库
接下来我们配置 hardhat 的配置文件:
// hardhat.config.js
require("@nomiclabs/hardhat-ethers");
require('@openzeppelin/hardhat-upgrades');
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.8.4",
networks: {
rinkeby: {
url: // 这里填写对应网络的 rpc 地址,
accounts: [这里填写私钥]
}
}
};
然后我们再来编写可升级合约,在 contracts 文件夹下创建 Demo.sol 文件:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.4;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract Demo is Initializable {
uint256 public a;
// 初始化函数,后面的修饰符 initializer 来自 Initializable.sol
// 用于限制该函数只能调用一次
function initialize(uint256 _a) public initializer {
a = _a;
}
function increaseA() external {
++a;
}
}
编译合约 npx hardhat compile,没有问题。
接着我们编写部署脚本,在 scripts 文件夹下创建 deploy.js 文件:
async function main() {
const Demo = await ethers.getContractFactory("Demo");
console.log("Deploying Demo...");
// initializer 后面的参数为初始化函数的名字,这里为 initialize
// 中括号的参数为初始化函数的参数
const demo = await upgrades.deployProxy(Demo, [101], { initializer: 'initialize' });
// 这里打印的地址为代理合约的地址
console.log("Demo deployed to:", demo.address);
}
// 这里也可以简化为 main(),后面的都省略也可以
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
我们来运行部署脚本,这里我们使用本地测试网络进行部署:
注意,本地测试网络需要运行 npx hardhat node。本地网络无需在配置文件中配置,如果使用真实网络,需要进行配置,并且在 --network 后面指定网络名称即可。
npx hardhat run scripts/deploy.js --network localhost
可以观察到一共部署了三个合约,对应的部署顺序分别是
逻辑合约
ProxyAdmin 合约
代理合约(名为 TransparentUpgradeableProxy)
注意,一个项目中只会有一个 ProxyAdmin 合约,管理着所有的代理合约。也就是说,我们在同一个项目中再去部署另外的合约,那么只会有步骤 1、3,ProxyAdmin 只在部署第一个合约时会部署。
这时,假设由于业务场景变化,需要修改合约,将 increaseA 函数修改为:
function increaseA() external {
a += 10;
}
再次编译,没有问题。接着编写升级脚本,在 scripts 文件夹下创建 upgrade.js 文件:
async function main() {
// 这里的地址为前面部署的代理合约地址
const proxyAddress = '0x...';
const Demo = await ethers.getContractFactory("Demo");
console.log("Preparing upgrade...");
// 升级合约
await upgrades.upgradeProxy(proxyAddress, Demo);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
接着运行
npx hardhat run scripts/upgrade.js --network localhost
升级合约时, upgradeProxy 中一共有两个步骤:
部署新的逻辑合约
调用 ProxyAdmin 合约的 upgrade 函数来更换新合约,两个参数分别是代理合约和新逻辑合约的地址
这样我们就完成了可升级合约的部署与升级,注意到我们的部署过程中有 ProxyAdmin 合约,说明这是 TransparentProxy 的合约升级模式。接下来我们看看 UUPS 的模式如何部署及升级。
若要支持 UUPS 的升级模式,需要做以下几点改动:
逻辑合约需继承 UUPSUpgradeable 合约
覆写 _authorizeUpgrade 函数
部署脚本需要添加 kind: 'uups' 参数
此时,逻辑合约变为:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.4;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
// 需要继承 UUPSUpgradeable 合约
contract Demo is Initializable, UUPSUpgradeable {
uint256 public a;
function initialize(uint256 _a) public initializer {
a = _a;
}
function increaseA() external {
a += 10;
}
// 覆写 _authorizeUpgrade 函数
function _authorizeUpgrade(address) internal override {}
}
部署脚本变为:
async function main() {
const Demo = await ethers.getContractFactory("Demo");
console.log("Deploying Demo...");
// 这里添加了参数 => kind: 'uups'
const demo = await upgrades.deployProxy(Demo, [101], { initializer: 'initialize', kind: 'uups' });
console.log("Demo deployed to:", demo.address);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
编译合约并运行部署脚本(注意,这时最好删除 .openzeppelin 文件夹下的对应网络配置文件,因为其包含了我们上面测试的 TransparentProxy 模式的一些运行配置,可能会有影响),可以观察到一共部署了两个合约,分别是:
逻辑合约
代理合约(名为 ERC1967Proxy)
此时,假设我们需要升级合约,在改动了合约之后,我们可以继续使用上面的 upgrade.js 脚本进行升级,这时的升级步骤是:
部署新的逻辑合约
调用代理合约的 upgradeTo 函数进行升级,参数是新的逻辑合约地址
我们可以看到,两种升级模式有所区别。TransparentProxy 模式在升级的时候,需要调用 ProxyAdmin 的升级函数。而 UUPS 模式在升级时,需要调用代理合约的升级函数。后者相比于前者少部署一个合约。
这里有一个重点是,由于 TransparentProxy 模式是由 ProxyAdmin 进行管理,也就是说只有 ProxyAdmin 有权限进行升级,那么我们只要保证 ProxyAdmin 合约的管理员权限安全即可保证整个可升级架构安全。而对于 UUPS 模式来说,升级合约的逻辑是需要调用代理合约的,这时的权限管理就需要开发者手动处理。具体来说,就是对于我们覆写的 _authorizeUpgrade 函数,需要加上权限管理:
function _authorizeUpgrade(address) internal
override onlyOwner {}
这里加上了 onlyOwner 用于限制升级权限,否则任何人都可调用代理合约的 upgradeTo 进行升级。但要注意的是,我们这里只是简单加上了 onlyOwner 做为示例的权限管理,在实际开发中,由于升级的逻辑和业务逻辑都在逻辑合约中,因此需要区分业务场景的 owner 和合约升级架构的 owner。这里可能会对开发者带来困扰,因此需要多加注意。
TransparentProxy 和 UUPS 是目前阶段比较流行的成熟的合约升级解决方案。OZ 建议开发者使用 UUPS,更加轻量级,节省 gas。我个人还是比较倾向于前者,因为我觉得前者架构清晰,权限管理简单。后者将业务逻辑和升级组件都放在逻辑合约中,当需要多次升级合约时,是否节省 gas 仍需探讨。不过建议大家两个都能够熟练掌握,毕竟难度不高,上手也很简单。
欢迎和我交流
https://www.npmjs.com/package/@openzeppelin/hardhat-upgrades
前面的文章我们基本把合约升级的原理介绍完了,这篇文章我们来实际操作一下,部署一个可升级合约。我们将会使用到 hardhat 框架和 OpenZeppelin 的可升级合约库。这个库和 OZ 的普通合约库的区别是,所有的合约中都没有构造函数,作为代替的是 initialize 函数,用来作初始化操作。
首先执行下面的命令来做一些初始化的工作:
mkdir upgradeable_demo && cd upgradeable_demo
npm init -y
npm install --save-dev hardhat
npx hardhat,创建实例项目,并且按照步骤进行
npm install --save-dev @openzeppelin/hardhat-upgrades,安装 hardhat 的升级组件
npm install --save-dev @nomiclabs/hardhat-ethers ethers,这两个包主要用于合约部署和测试
npm install --save @openzeppelin/contracts-upgradeable,安装可升级合约库
接下来我们配置 hardhat 的配置文件:
// hardhat.config.js
require("@nomiclabs/hardhat-ethers");
require('@openzeppelin/hardhat-upgrades');
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.8.4",
networks: {
rinkeby: {
url: // 这里填写对应网络的 rpc 地址,
accounts: [这里填写私钥]
}
}
};
然后我们再来编写可升级合约,在 contracts 文件夹下创建 Demo.sol 文件:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.4;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract Demo is Initializable {
uint256 public a;
// 初始化函数,后面的修饰符 initializer 来自 Initializable.sol
// 用于限制该函数只能调用一次
function initialize(uint256 _a) public initializer {
a = _a;
}
function increaseA() external {
++a;
}
}
编译合约 npx hardhat compile,没有问题。
接着我们编写部署脚本,在 scripts 文件夹下创建 deploy.js 文件:
async function main() {
const Demo = await ethers.getContractFactory("Demo");
console.log("Deploying Demo...");
// initializer 后面的参数为初始化函数的名字,这里为 initialize
// 中括号的参数为初始化函数的参数
const demo = await upgrades.deployProxy(Demo, [101], { initializer: 'initialize' });
// 这里打印的地址为代理合约的地址
console.log("Demo deployed to:", demo.address);
}
// 这里也可以简化为 main(),后面的都省略也可以
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
我们来运行部署脚本,这里我们使用本地测试网络进行部署:
注意,本地测试网络需要运行 npx hardhat node。本地网络无需在配置文件中配置,如果使用真实网络,需要进行配置,并且在 --network 后面指定网络名称即可。
npx hardhat run scripts/deploy.js --network localhost
可以观察到一共部署了三个合约,对应的部署顺序分别是
逻辑合约
ProxyAdmin 合约
代理合约(名为 TransparentUpgradeableProxy)
注意,一个项目中只会有一个 ProxyAdmin 合约,管理着所有的代理合约。也就是说,我们在同一个项目中再去部署另外的合约,那么只会有步骤 1、3,ProxyAdmin 只在部署第一个合约时会部署。
这时,假设由于业务场景变化,需要修改合约,将 increaseA 函数修改为:
function increaseA() external {
a += 10;
}
再次编译,没有问题。接着编写升级脚本,在 scripts 文件夹下创建 upgrade.js 文件:
async function main() {
// 这里的地址为前面部署的代理合约地址
const proxyAddress = '0x...';
const Demo = await ethers.getContractFactory("Demo");
console.log("Preparing upgrade...");
// 升级合约
await upgrades.upgradeProxy(proxyAddress, Demo);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
接着运行
npx hardhat run scripts/upgrade.js --network localhost
升级合约时, upgradeProxy 中一共有两个步骤:
部署新的逻辑合约
调用 ProxyAdmin 合约的 upgrade 函数来更换新合约,两个参数分别是代理合约和新逻辑合约的地址
这样我们就完成了可升级合约的部署与升级,注意到我们的部署过程中有 ProxyAdmin 合约,说明这是 TransparentProxy 的合约升级模式。接下来我们看看 UUPS 的模式如何部署及升级。
若要支持 UUPS 的升级模式,需要做以下几点改动:
逻辑合约需继承 UUPSUpgradeable 合约
覆写 _authorizeUpgrade 函数
部署脚本需要添加 kind: 'uups' 参数
此时,逻辑合约变为:
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.4;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
// 需要继承 UUPSUpgradeable 合约
contract Demo is Initializable, UUPSUpgradeable {
uint256 public a;
function initialize(uint256 _a) public initializer {
a = _a;
}
function increaseA() external {
a += 10;
}
// 覆写 _authorizeUpgrade 函数
function _authorizeUpgrade(address) internal override {}
}
部署脚本变为:
async function main() {
const Demo = await ethers.getContractFactory("Demo");
console.log("Deploying Demo...");
// 这里添加了参数 => kind: 'uups'
const demo = await upgrades.deployProxy(Demo, [101], { initializer: 'initialize', kind: 'uups' });
console.log("Demo deployed to:", demo.address);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
编译合约并运行部署脚本(注意,这时最好删除 .openzeppelin 文件夹下的对应网络配置文件,因为其包含了我们上面测试的 TransparentProxy 模式的一些运行配置,可能会有影响),可以观察到一共部署了两个合约,分别是:
逻辑合约
代理合约(名为 ERC1967Proxy)
此时,假设我们需要升级合约,在改动了合约之后,我们可以继续使用上面的 upgrade.js 脚本进行升级,这时的升级步骤是:
部署新的逻辑合约
调用代理合约的 upgradeTo 函数进行升级,参数是新的逻辑合约地址
我们可以看到,两种升级模式有所区别。TransparentProxy 模式在升级的时候,需要调用 ProxyAdmin 的升级函数。而 UUPS 模式在升级时,需要调用代理合约的升级函数。后者相比于前者少部署一个合约。
这里有一个重点是,由于 TransparentProxy 模式是由 ProxyAdmin 进行管理,也就是说只有 ProxyAdmin 有权限进行升级,那么我们只要保证 ProxyAdmin 合约的管理员权限安全即可保证整个可升级架构安全。而对于 UUPS 模式来说,升级合约的逻辑是需要调用代理合约的,这时的权限管理就需要开发者手动处理。具体来说,就是对于我们覆写的 _authorizeUpgrade 函数,需要加上权限管理:
function _authorizeUpgrade(address) internal
override onlyOwner {}
这里加上了 onlyOwner 用于限制升级权限,否则任何人都可调用代理合约的 upgradeTo 进行升级。但要注意的是,我们这里只是简单加上了 onlyOwner 做为示例的权限管理,在实际开发中,由于升级的逻辑和业务逻辑都在逻辑合约中,因此需要区分业务场景的 owner 和合约升级架构的 owner。这里可能会对开发者带来困扰,因此需要多加注意。
TransparentProxy 和 UUPS 是目前阶段比较流行的成熟的合约升级解决方案。OZ 建议开发者使用 UUPS,更加轻量级,节省 gas。我个人还是比较倾向于前者,因为我觉得前者架构清晰,权限管理简单。后者将业务逻辑和升级组件都放在逻辑合约中,当需要多次升级合约时,是否节省 gas 仍需探讨。不过建议大家两个都能够熟练掌握,毕竟难度不高,上手也很简单。
欢迎和我交流
https://www.npmjs.com/package/@openzeppelin/hardhat-upgrades
EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
Smart Contract Developer
Smart Contract Developer
Share Dialog
Share Dialog

Subscribe to xyyme.eth

Subscribe to xyyme.eth
<100 subscribers
<100 subscribers
No activity yet