文章目录
原文链接:https://trufflesuite.com/guides/upgrading-security/
升级智能合约 - 您应该如何做?
在本指南中,我们将讨论智能合约升级:它们是什么、这样做的安全隐患以及如何智能合约升级!
在YouTube 上观看我们与来自OpenZeppelin的安全解决方案架构师Michael Lewellen的现场直播,以获得更深入的解释和采访!如果您决定编写可升级的智能合约,我们将进一步详细介绍不同类型的升级模式以及技巧!
什么是智能合约升级?
默认情况下,智能合约是不可变的,这对于免信任、去中心化和安全性的以太坊是必要的。但是,当您发现智能合约漏洞时怎么办?或者,如果您想添加新的特性和功能怎么办?智能合约升级本质是人们可采取的改变合约功能的策略。请注意,如果要更改合约代码,必须以可升级的方式部署初始合约。不同于更改内部代码。可升级, 意味着您正在更改执行的代码。进行广泛的研究,发现编写可升级智能合约的各种方法或模式,同时尽量减少中心化和特定的安全风险。了解更多,参考OpenZeppelin 有一篇很棒的文章在这里。
你应该升级智能合约吗?
权衡是什么?
在深入探讨我们如何升级之前,我们应该首先考虑是否应该进行合约升级。可升级智能合约的优点主要分为两类:
- 部署后发现的漏洞修复起来更容易、更快。
- 随着时间的推移,开发人员可以通过试验和添加新功能来改进他们的 dapp。
虽然这听起来不错,但违反不变性会通过以下方式影响免信任、安全和去中心化:
- 因为开发人员可以更改代码,所以用户必须相信开发人员不会恶意或任意地这样做。
- 编写可升级的智能合约是困难和复杂的。因此,开发人员可能会引入比其他情况下更多的缺陷。
- 如果升级合约的能力不安全或中心化,攻击者很容易进行恶意升级。
最后,根据您升级合约的方式,您可能会产生高昂的 gas 成本。
你如何决定升级?
在考虑到智能合约升级的影响后,下一步就是实际做出是否需要升级合约的决定。至关重要的是,这个决定不会落在单个帐户的手中。单一账户不仅会颠覆去中心化,而且泄露的密钥也会对合约的安全性造成灾难性的后果。
有几种流行的升级方式:
1.Multi-sig:多重签名合约允许有多个所有者,一旦达到一定阈值的利益相关者同意,就会做出决定。
2. Timelock :时间锁指的是更改实际生效的时间延迟。这让用户有时间在不同意更改时退出。然而,时间锁会带来两个问题:
- 延迟可能是对严重错误做出快速响应的主要障碍。
- 这可以通过暂停和逃生舱来缓解。在这种情况下,受信任的开发人员可以在检测到问题后立即暂停操作,例如停止令牌传输,以防止造成更多伤害。 同时,用户可以使用编码到智能合约中的逃生舱口退出系统,例如提取资金。
- 发布稍后添加的时间锁定升级允许攻击者对更改进行逆向工程,并可能在更改生效之前利用该错误。在这种情况下,通过宣布升级来使用提交显示策略,但在延迟到期之前不会显示它。
3. Voting:投票通过授予您的社区对更改进行投票的权利来进一步去中心化决策,通常通过一些治理令牌来完成。请注意,这通常与上面列出的其他策略结合使用。
如何升级智能合约?
如前所述,此处列出了许多技术上复杂的升级模式。
它的核心是,升级模式依赖于代理合约和执行合约(又名逻辑合约)。代理合约知道执行合约的合约地址,并将它收到的所有调用都委托给它。 这意味着:
- 执行合约代码的执行发生在代理合同的上下文中。
- 读取或写入存储只影响代理合约的存储,不影响执行合约。
msg.sender
是调用代理合约的人的地址
这一切都是可能的,因为操作码DELEGATECALL
,它允许一个合约执行来自另一个合约的代码,就好像它是一个内部函数一样。 因此,升级实际上相对简单——只需更改执行合约地址即可。当考虑实际的升级逻辑时,就比较复杂性了。
我们不会深入研究它的所有情况,但OpenZeppelin Upgrades 插件使用透明代理模式模式,您可以在此处和此处阅读更多信息。
现在,让我们实际来看一个例子!完成的代码在这里。
下载安装所需工具
你需要安装:
- Node.js, v12 或更高版本
- truffle
- ganache UI 或者ganache CLI
创建一个 Infura 帐户和项目
要将你的 DApp 连接到以太网和测试网,你需要一个 Infura 帐户。在这里注册一个帐户。
然后登录,创建一个项目! 我们将其命名为 rentable-nft
,并从下拉列表中选择 Web3API
注册 MetaMask 钱包
要在浏览器中与你的 DApp 交互,你需要一个 MetaMask 钱包。
下载VS Code
您可以随意使用任何您想要的 IDE,但是我们强烈推荐使用 VS Code!您可以使用 Truffle 扩展运行本教程的大部分内容来创建、构建和部署智能契约,而不需要使用 CLI!你可以在这里了解更多。
获取测试网eth
为了部署到公共测试网,你需要一些测试 Eth 来支付你的gas费用!Paradigm 有一个很棒的 MultiFaucet,它可以同时在8个不同的网络上存入资金。
配置项目
Truffle 可以为您的项目搭建脚手架,并添加示例合约和测试。我们将在一个名为 upgrade-contract
的文件夹中构建项目。
truffle init upgrade-contract
cd upgrade-contract
truffle create contract UpgradeablePet
truffle create test UpgradeablePet
执行完成后,项目结构如下:
upgrade-contract
├── contracts
│ └── UpgradeablePet.sol
├── migrations
│ └── 1_deploy_contracts.js
├── test
│ └── upgradeable_pet.js
└── truffle-config.js
编写可升级合约 V1
让我们从写基本合约开始,我们将逐步升级!
首先,我们的合约需要安全升级。这意味着合约:
- 不能有构造函数
- 不应该使用
selfdestruct
和delegatecall
操作
您可以在此处阅读有关原因的更多信息。
我们的UpgradeablePet
第一次迭代将非常简单 - 它所做的就是存储一个值并获取该值。如下所示:
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
contract UpgradeablePet {
uint256 private _value;
// Emitted when the stored value changes
event ValueChanged(uint256 value);
// Stores a new value in the contract
function store(uint256 value) public {
_value = value;
emit ValueChanged(value);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return _value;
}
}
假设我们只希望宠物主人能够更改UpgradeablePet
的内容。如果我们没有构造函数,我们如何传入适当的地址?OpenZeppelin 提供了一个名为Initializer的基础合约,它将帮助我们运行必要的初始化代码。
首先,我们需要安装:
npm i @openzeppelin/contracts-upgradeable
然后,我们可以修改UpgradeablePet
:
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract UpgradeablePet is Initializable {
uint256 private _value;
address private _petOwner;
// Emitted when the stored value changes
event ValueChanged(uint256 value);
function initialize(address petOwner) public initializer {
_petOwner = petOwner;
}
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
// Stores a new value in the contract
function store(uint256 value) public {
require(msg.sender == _petOwner, "UpgradeablePet: not owner");
_value = value;
emit ValueChanged(value);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return _value;
}
}
需要注意两点:
- 如果有任何父合约,则必须手动调用这些父合约的
initalize
。 initialize
确保了合约处于初始化状态。否则,未初始化的执行合约可能会被攻击者接管。
现在,我们需要修改1_deploy_contracts.js
以告诉 Truffle 如何部署此文件。我们首先需要下载插件:
npm i --save-dev @openzeppelin/truffle-upgrades
然后,按以下方式修改1_deploy_contracts.js
文件:
const {
deployProxy } = require('@openzeppelin/truffle-upgrades');
const UpgradeablePet = artifacts.require('UpgradeablePet');
module.exports = async function (deployer, network, accounts) {
await deployProxy(UpgradeablePet, [accounts[0]], {
deployer, initializer: 'initialize' });
};
为了对此进行测试,我们将即时执行此操作。您可以调用truffle develop
,这将在 9545
上调出一个 ganache 实例,或者打开您自己的 ganache 实例,在truffle-config.js
中进行修改development,然后运行truffle console
。对于本指南,我们建议打开一个单独的 ganache 实例,以便保留合约地址。
truffle(develop)> migrate
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Starting migrations...
======================
> Network name: 'development'
> Network id: 1660859525632
> Block gas limit: 30000000 (0x1c9c380)
1_deploy_contracts.js
=====================
Replacing 'UpgradeablePet'
--------------------------
> transaction hash: 0xb42a280a989a089efb526930b1da5f80cd41a487f4b0facd9d6ccc376e273f56
> Blocks: 0 Seconds: 0
> contract address: 0xc094d30a290db2C0781fF97874D35A6dF8c0F225
> block number: 11
> block timestamp: 1660863108
> account: 0xA8469E3bF6474abb1290a4c03F43021667df130e
> balance: 999.985882023380156735
> gas used: 410834 (0x644d2)
> gas price: 2.739722993 gwei
> value sent: 0 ETH
> total cost: 0.001125571356106162 ETH
Deploying 'ProxyAdmin'
----------------------
> transaction hash: 0xac44c24fa0ca5e3118e1c027474e55584c8ec13e48e797e315d6812d5ccc94b6
> Blocks: 0 Seconds: 0
> contract address: 0x749D40F055727817e9E9D56e5247722407ccae17
> block number: 12
> block timestamp: 1660863108
> account: 0xA8469E3bF6474abb1290a4c03F43021667df130e
> balance: 999.984570049252513955
> gas used: 484020 (0x762b4)
> gas price: 2.710578339 gwei
> value sent: 0 ETH
> total cost: 0.00131197412764278 ETH
Deploying 'TransparentUpgradeableProxy'
---------------------------------------
> transaction hash: 0xd82a7bcec734f0e69d614016d3408a64ef564082d328215e4f79e8669934f9c7
> Blocks: 0 Seconds: 0
> contract address: 0xb85a509102B82f02281b0451C43FA37e00d625ad
> block number: 13
> block timestamp: 1660863108
> account: 0xA8469E3bF6474abb1290a4c03F43021667df130e
> balance: 999.982844095713016935
> gas used: 642788 (0x9cee4)
> gas price: 2.685105415 gwei
> value sent: 0 ETH
> total cost: 0.00172595353949702 ETH
> Saving artifacts
-------------------------------------
> Total cost: 0.004163499023245962 ETH
Summary
=======
> Total deployments: 3
> Final cost: 0.004163499023245962 ETH
deployProxy
做了三件事:
- 部署执行合约(Box 合约)
- 部署 ProxyAdmin 合约(代理的admin)。
- 部署代理合约并运行任何初始化函数。
现在,我们可以直接从控制台调用合约函数来快速查看我们的合约是否正常工作。
truffle(development)> let contract = await UpgradeablePet.deployed();
undefined
truffle(development)> await contract.store(5)
{
tx: '0xeb7971ae96003a2be24ed38e7d62ab8741f5a8f772d5155679f41929d2808a6f',
receipt: {
transactionHash: '0xeb7971ae96003a2be24ed38e7d62ab8741f5a8f772d5155679f41929d2808a6f',
transactionIndex: 0,
blockNumber: 14,
blockHash: '0x5ebca4e4c9d5bed1300e6fe9399144f47f8f751ce2bd4a655b160242cb540e44',
from: '0xa8469e3bf6474abb1290a4c03f43021667df130e',
to: '0xb85a509102b82f02281b0451c43fa37e00d625ad',
cumulativeGasUsed: 54413,
gasUsed: 54413,
contractAddress: null,
logs: [ [Object] ],
logsBloom: '0x40000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000040000000000000000000000400000000000000000000000000000',
status: true,
effectiveGasPrice: 2662958768,
type: '0x2',
rawLogs: [ [Object] ]
},
logs: [
{
address: '0xb85a509102B82f02281b0451C43FA37e00d625ad',
blockHash: '0x5ebca4e4c9d5bed1300e6fe9399144f47f8f751ce2bd4a655b160242cb540e44',
blockNumber: 14,
logIndex: 0,
removed: false,
transactionHash: '0xeb7971ae96003a2be24ed38e7d62ab8741f5a8f772d5155679f41929d2808a6f',
transactionIndex: 0,
id: 'log_c488ac08',
event: 'ValueChanged',
args: [Result]
}
]
}
truffle(development)> contract.retrieve()
BN {
negative: 0, words: [ 5, <1 empty item> ], length: 1, red: null }
好的!在继续之前,让我们写一个测试!在为升级编写测试时,我们需要为执行合约和代理合约编写测试。幸运的是,我们可以在测试中使用OpenZeppelin
的deployProxy
。
让我们首先安装 OpenZeppelin 的测试助手,使测试更容易一些。
npm i --save-dev @openzeppelin/test-helpers
为执行合约编写测试与往常一样。我们在该文件夹下创建一个名为upgradeable_pets.js
的test
文件并添加以下代码:
const {
expectRevert, expectEvent } = require('@openzeppelin/test-helpers');
const UpgradeablePet = artifacts.require("UpgradeablePet");
contract("UpgradeablePet", function (accounts) {
it("should retrieve correctly stored value", async function () {
const upgradeablePetInstance = await UpgradeablePet.deployed();
let tx = await upgradeablePetInstance.store(5);
expectEvent(tx, "ValueChanged", {
value: "5" });
let value = await upgradeablePetInstance.retrieve();
assert.equal(value, 5, "UpgradeablePet did not store correct value");
});
it("should not set the stored value if not owner", async function () {
const upgradeablePetInstance = await UpgradeablePet.deployed();
// Failed require in function
await expectRevert(upgradeablePetInstance.store(10, {
from: accounts[1]}), "UpgradeablePet: not owner");
let value = await upgradeablePetInstance.retrieve();
assert.equal(value, 5, "UpgradeablePet value should not have changed");
});
});
然后,创建一个名为的测试文件upgradeable_pets.proxy.js
,并添加以下内容:
const {
expectRevert, expectEvent } = require('@openzeppelin/test-helpers');
const {
deployProxy } = require('@openzeppelin/truffle-upgrades');
const UpgradeablePet = artifacts.require("UpgradeablePet");
contract("UpgradeablePet (Proxy)", function (accounts) {
it("should retrieve correctly stored value", async function () {
const upgradeablePetInstance = await deployProxy(UpgradeablePet, [accounts[0]], {
initializer: 'initialize' });
let tx = await upgradeablePetInstance.store(5);
expectEvent(tx, "ValueChanged", {
value: "5" });
let value = await upgradeablePetInstance.retrieve();
assert.equal(value, 5, "UpgradeablePet did not store correct value");
});
it("should not set the stored value if not owner", async function () {
const upgradeablePetInstance = await deployProxy(UpgradeablePet, [accounts[0]], {
initializer: 'initialize' });
// Failed require in function
await expectRevert(upgradeablePetInstance.store(10, {
from: accounts[1]}), "UpgradeablePet: not owner");
let value = await upgradeablePetInstance.retrieve();
assert.equal(value, 0, "UpgradeablePet value should not have changed");
});
});
您会注意到,我们没有使用pgradeablePet.deployed()
,而是使用deployProxy
来获取合约实例。
进行测试,只需调用truffle test
。
编写可升级合约 V2
现在,让我们来到令人兴奋的部分:添加更改!我们将首先创建一个新的重复合约,然后添加一个increment
函数以增加存储的值。
新合约UpgradeablePetV2
有一个increment
方法,如下:
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract UpgradeablePetV2 is Initializable {
uint256 private _value;
address private _petOwner;
// Emitted when the stored value changes
event ValueChanged(uint256 value);
function initialize(address petOwner) public initializer {
_petOwner = petOwner;
}
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {
}
// Stores a new value in the contract
function store(uint256 value) public {
require(msg.sender == _petOwner, "UpgradeablePet: not owner");
_value = value;
emit ValueChanged(value);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return _value;
}
// Increments the stored value by 1
function increment() public {
_value = _value + 1;
emit ValueChanged(_value);
}
}
现在,我们将使用OpenZeppelin
的upgradeProxy
函数,该功能将:
- 部署执行合约(
UpgradeablePetV2
) - 调用
ProxyAdmin
更新代理合约以使用新的实现。
我们的新部署脚本:
const {
upgradeProxy } = require('@openzeppelin/truffle-upgrades');
const UpgradeablePet = artifacts.require('UpgradeablePet');
const UpgradeablePetV2 = artifacts.require('UpgradeablePetV2');
module.exports = async function (deployer) {
const alreadyDeployed = await UpgradeablePet.deployed();
await upgradeProxy(alreadyDeployed.address, UpgradeablePetV2, {
deployer });
};
然后,调用migrate
,并测试increment
:
truffle(development)> contract = await UpgradeablePet.deployed()
undefined
truffle(development)> contract.address
'0xAe02BB114AAD3Edf8b87827Cf001F3D49165b426'
truffle(development)> let contractv2 = await UpgradeablePetV2.at(contract.address)
undefined
truffle(development)> contractv2.retrieve()
BN {
negative: 0, words: [ 5, <1 empty item> ], length: 1, red: null }
truffle(development)> contractv2.increment()
{
tx: '0x76820ff204a1ba364ee3deed7e62371f0803eb0b661fd5d88d845abb3f972fbc',
receipt: {
transactionHash: '0x76820ff204a1ba364ee3deed7e62371f0803eb0b661fd5d88d845abb3f972fbc',
transactionIndex: 0,
blockNumber: 36,
blockHash: '0xe75e2d55a8d310f6686c61f5f8ecf75e05b8be96b985ac31f515a90eafe058d2',
from: '0xa8469e3bf6474abb1290a4c03f43021667df130e',
to: '0xae02bb114aad3edf8b87827cf001f3d49165b426',
cumulativeGasUsed: 35076,
gasUsed: 35076,
contractAddress: null,
logs: [ [Object] ],
logsBloom: '0x40000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000200',
status: true,
effectiveGasPrice: 2509015503,
type: '0x2',
rawLogs: [ [Object] ]
},
logs: [
{
address: '0xAe02BB114AAD3Edf8b87827Cf001F3D49165b426',
blockHash: '0xe75e2d55a8d310f6686c61f5f8ecf75e05b8be96b985ac31f515a90eafe058d2',
blockNumber: 36,
logIndex: 0,
removed: false,
transactionHash: '0x76820ff204a1ba364ee3deed7e62371f0803eb0b661fd5d88d845abb3f972fbc',
transactionIndex: 0,
id: 'log_b88893d4',
event: 'ValueChanged',
args: [Result]
}
]
}
truffle(development)> contractv2.retrieve()
BN {
negative: 0, words: [ 6, <1 empty item> ], length: 1, red: null }
请注意,我们使用了et contractv2 = await UpgradeablePetV2.at(contract.address). .at
是一个特殊的Truffle 函数,可让您在同一地址创建新的抽象。
最后,让我们写测试用例。同样,我们需要测试执行合约和代理合约。
创建upgradeable_pet_V2.js
,如下:
const {
expectEvent } = require('@openzeppelin/test-helpers');
const UpgradeablePetV2 = artifacts.require("UpgradeablePetV2");
contract("UpgradeablePetV2", function (accounts) {
it("should increment the stored value", async function () {
const upgradeablePetV2Instance = await UpgradeablePetV2.deployed();
let tx = await upgradeablePetV2Instance.store(5);
expectEvent(tx, "ValueChanged", {
value: "5" });
let value = await upgradeablePetV2Instance.retrieve();
assert.equal(value, 5, "UpgradeablePetV2 did not store correct value");
await upgradeablePetV2Instance.increment();
value = await upgradeablePetV2Instance.retrieve();
assert.equal(value, 6, "UpgradeablePetV2 did not increment");
});
});
像deployProxy
一样,我们还可以在测试中使用升级。创建一个名为upgradeable_pet_V2.proxy.js
的新测试:
const {
deployProxy, upgradeProxy } = require('@openzeppelin/truffle-upgrades');
const UpgradeablePet = artifacts.require("UpgradeablePet");
const UpgradeablePetV2 = artifacts.require("UpgradeablePetV2");
contract("UpgradeablePetV2 (Proxy)", function (accounts) {
it("should increment the stored value", async function () {
const upgradeablePetInstance = await deployProxy(UpgradeablePet, [accounts[0]], {
initializer: 'initialize' });
await upgradeablePetInstance.store(5);
let value = await upgradeablePetInstance.retrieve();
assert.equal(value, 5, "UpgradeablePet did not store correct value");
const upgradeablePetV2Instance = await upgradeProxy(upgradeablePetInstance.address, UpgradeablePetV2);
value = await upgradeablePetV2Instance.retrieve();
assert.equal(value, 5, "UpgradeablePetV2 did not store correct value");
await upgradeablePetV2Instance.increment();
value = await upgradeablePetV2Instance.retrieve();
assert.equal(value, 6, "UpgradeablePetV2 did not increment");
});
});
我们要在这里测试的点是,在智能合约的V1和V2之间保留了状态。
未来扩展
恭喜你!您已经升级了一份智能合约!同样,请务必在YouTube上观看直播,并查看我们的GitHub上的内容。Openzeppelin还撰写了自己的博客文章,该文章更加深入,并且包含真实的示例。如果您有兴趣升级之前的合约,则需要使用其基本合约的ugpradable版本,通过npm i @open-zeppelin/upgradeable-contract
安装,该合约将使用initialize
而不是constructor
。
如果你想讨论这个内容,对你想看到的内容提出建议或者问一些关于这个系列的问题,在这里讨论。如果你想展示你建立了什么以及想加入Unleashed社区,加入我们的Discord!最后,别忘了在 Twitter 上关注我们关于 Truffle 的最新消息。