Why write upgradable contracts
Smart contracts in Ethereum are immutable by default. However, once the project party discovers contract loopholes in advance or wants to upgrade functions, the contract needs to be changeable, so it is important to write an upgradeable contract at the beginning. So we need to use upgradable contracts to enhance maintainability.
Upgrade contract overview
The upgrade contract is usually implemented using the proxy mode. There are two contracts in the working principle of this mode, one is the proxy contract, and the other is the implementation contract. The proxy contract is responsible for managing the contract status data, while the implementation contract is only responsible for executing the contract logic and does not store any state data. The user calls the proxy contract, and the proxy contract implements the contract delegate call
to achieve the purpose of upgrading.
At present, there are mainly 3 ways to replace/upgrade the implementation contract:
- Diamond Implementation
- Transparent Proxy Implementation
- UUPS Implementation
At present, transparent proxy implementation and UUPS implementation are commonly used. The purpose is to replace the address of the implementation contract with a new one (upgraded contract). The transparent proxy method is to put the update implementation contract function in the proxy contract, and UUPS updatate to address
is Put the update implementation contract in the implementation contract.
transparent proxy
Transparent Proxy ( EIP1967 ) is an easy way to separate responsibilities between proxy contracts and contracts. In this case, upgradeTo
the function is part of the proxy contract, and the implementing contract can upgradeTo
be , changing where future function calls delegate.
However, there are some caveats. If the proxy contract and the implementation contract have a function with the same name and parameters , in the transparent proxy contract, this problem is handled by the proxy contract. The proxy contract msg.sender
determines whether the user's call is executed in the proxy contract itself or in the implementation contract according to global variables. .
So if msg.sender
it is the admin of the proxy, then the proxy will not delegate the call, if it is not the admin address, the proxy will delegate the call to the implementing contract, even if it matches one of the proxy's functions.
So there is this problem with transparent proxies: owner
the address must be stored in memory, and using memory is one of the least efficient and expensive steps of interacting with a smart contract, every time a user calls the proxy, the proxy checks if the user is an administrator, This adds unnecessary gas costs to most transactions that occur. ( All in all, the cost is relatively high )
UUPS
UUPS Proxy ( EIP1822 ) is another way to separate responsibilities between proxy contracts and contracts. In the UUPS proxy mode, upgradeTo
the function is part of the implementation contract and is used by the user through the proxy contract delegatecall
.
In UUPS, whether it is an administrator or a user, all calls are sent to the implementation contract. The advantage of this is that every time we call, we don't have to access the storage space to check whether the user who started the call is an administrator, which improves efficiency and cost. Timelock
In addition, because it is to implement the contract, you can customize the function according to your needs, and add such as , etc. to each new implementation Access Control
, which cannot be done in the transparent proxy.
The problem with the UUPS proxy is that upgradeTo
the function exists in the implementation contract, which will add a lot of code and be easily attacked, and if the developer forgets to add this function, the contract will no longer be able to be upgraded.
Writing Upgradable Smart Contracts Using OpenZeppelin
Transparent Proxy in Action
installation
hardhat
environment## 安装升级包 $ yarn add @openzeppelin/contracts @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades ## 配置文件 import { HardhatUserConfig } from 'hardhat/config' import '@nomicfoundation/hardhat-toolbox' import '@openzeppelin/hardhat-upgrades' const config: HardhatUserConfig = { solidity: '0.8.17' } export default config
Writing Upgradable Contracts
// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract OpenProxy is Initializable { uint public value; function initialize(uint _value) public initializer { value = _value; } function increaseValue() external { ++value; } }
department script
import { ethers, upgrades } from 'hardhat' // yarn hardhat run scripts/deploy_openProxy.ts --network localhost async function main() { const OpenProxy = await ethers.getContractFactory('OpenProxy') // 部署合约, 并调用初始化方法 const myOpenProxy = await upgrades.deployProxy(OpenProxy, [10], { initializer: 'initialize' }) // 代理合约地址 const proxyAddress = myOpenProxy.address // 实现合约地址 const implementationAddress = await upgrades.erc1967.getImplementationAddress(myOpenProxy.address) // proxyAdmin 合约地址 const adminAddress = await upgrades.erc1967.getAdminAddress(myOpenProxy.address) console.log(`proxyAddress: ${proxyAddress}`) console.log(`implementationAddress: ${implementationAddress}`) console.log(`adminAddress: ${adminAddress}`) } main().catch((error) => { console.error(error) process.exitCode = 1 })
Compile the contract & start the local node & deploy the contract on the local network
$ yarn hardhat compile $ yarn hardhat node $ yarn hardhat run scripts/proxy/open_proxy/openProxy.ts --network localhost ## 部署完毕 proxyAddress: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 implementationAddress: 0x5FbDB2315678afecb367f032d93F642f64180aa3 adminAddress: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
There are actually three contracts deployed:
- agency contract
- Realize the contract
ProxyAdmin
contract
ProxyAdmin
The contract is used to manage the proxy contract, including upgrading the contract and transferring contract ownership.
The steps to upgrade the contract are
- Deploy a new implementation contract,
- Call the upgrade-related methods in
ProxyAdmin
the contract to set a new implementation contract address.
new implementation contract
// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract OpenProxyV2 is Initializable { uint public value; function initialize(uint _value) public initializer { value = _value; } function increaseValue() external { --value; } }
upgrade script
import { ethers } from "hardhat"; import { upgrades } from "hardhat"; const proxyAddress = '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' async function main() { console.log(proxyAddress, " original proxy address") const OpenProxyV2 = await ethers.getContractFactory("OpenProxyV2") console.log("upgrade to OpenProxyV2...") const myOpenProxyV2 = await upgrades.upgradeProxy(proxyAddress, OpenProxyV2) console.log(myOpenProxyV2.address, " OpenProxyV2 address(should be the same)") console.log(await upgrades.erc1967.getImplementationAddress(myOpenProxyV2.address), " getImplementationAddress") console.log(await upgrades.erc1967.getAdminAddress(myOpenProxyV2.address), " getAdminAddress") } ...
Execute the contract upgrade script as follows:
0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 original proxy address upgrade to OpenProxyV2... 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 OpenProxyV2 address(should be the same) 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 getImplementationAddress 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 getAdminAddress
It can be found that the address of the proxy contract and the address of the admin contract have not changed, only the address of the implementation contract has changed
The above contract upgrades.deployProxy
deployed is the transparent proxy mode used by default. If you want to use UUPS proxy mode, you need to specify explicitly.
UUPS combat
The hardhat environment is still above, but there are two places that need to be changed:
write contract
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract LogicV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable { function initialize() public initializer { __Ownable_init(); __UUPSUpgradeable_init(); } /// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {} // 需要此方法来防止未经授权的升级,因为在 UUPS 模式中,升级是从实现合约完成的,而在透明代理模式中,升级是通过代理合约完成的 function _authorizeUpgrade(address) internal override onlyOwner {} mapping(string => uint256) private logic; event logicSetted(string indexed _key, uint256 _value); function SetLogic(string memory _key, uint256 _value) external { logic[_key] = _value; emit logicSetted(_key, _value); } function GetLogic(string memory _key) public view returns (uint256) { return logic[_key]; } }
Contract deployment script
## 只需要稍微变动一下 // 部署合约, 并调用初始化方法 const myLogicV1 = await upgrades.deployProxy(LogicV1, { initializer: 'initialize', kind: 'uups' })
Warning: A proxy admin was previously deployed on this network // 管理员合约实际不存在了,只有代理合约和实现合约 proxyAddress: 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853 implementationAddress: 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 adminAddress: 0x0000000000000000000000000000000000000000
When compiling and deploying a contract in UUPS proxy mode, only two contracts will actually be deployed
- agency contract
- Realize the contract
The steps to upgrade the contract at this time are
- Deploy a new implementation contract,
- Call the upgrade-related methods in
ProxyAdmin
the contract to set a new implementation contract address.
**************************
*****wx: mindcarver*******
*****公众号:区块链技术栈*****
**************************
reference
https://eips.ethereum.org/EIPS/eip-1822
https://eips.ethereum.org/EIPS/eip-1967
https://blog.openzeppelin.com/the-state-of-smart-contract-upgrades/
https://blog.gnosis.pm/solidity-delegateproxy-contracts-e09957d0f201
https://blog.openzeppelin.com/deep-dive-into-the-minimal-proxy-contract/
https://docs.openzeppelin.com/upgrades-plugins/1.x/api-hardhat-upgrades
https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#the-constructor-caveat