- 1. Tạo upgradeable contract với
@openzeppelin/hardhat-upgrade
plugin - 2. Tiến hành upgrade contract
- 3. Check code phân quyền trong Transparent Upgradeable Proxy
- 4. Tham khảo
1. Tạo upgradeable contract với @openzeppelin/hardhat-upgrade
plugin
Tạo hardhat project:
mkdir mycontract && cd mycontract
npm init -y
Install hardhat và các plugin:
npm install --save-dev hardhat
npm install --save-dev @openzeppelin/hardhat-upgrades
npm install --save-dev @nomicfoundation/hardhat-ethers ethers
Config hardhat.config.js
// hardhat.config.js
require("@nomicfoundation/hardhat-ethers");
require("@openzeppelin/hardhat-upgrades");
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: <infura-url here>,
accounts: [<private-key here>],
},
},
};
Viết contract Box
mkdir contracts
touch Box.sol
với nội dung đơn giản
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Box {
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
}
Viết script deploy Box contract
mkdir scripts
touch deployUpgradeableBox.js
với nội dung:
// scripts/deployUpgradeableBox.js
const { ethers, upgrades } = require("hardhat");
async function main() {
const Box = await ethers.getContractFactory("Box");
console.log("Deploying Box...");
const box = await upgrades.deployProxy(Box, [42], { initializer: "store" });
await box.waitForDeployment();
console.log("Box deployed to:", await box.getAddress());
}
main();
ở đây bước quan trọng nhất là bước upgrades.deployProxy(Box, [42], { initializer: "store" });
, hàm này sẽ deploy một TransparentUpgradeableProxy
với implementation (hay còn gọi là logic contract) là contract Box
, constructor cho implementation là hàm store
, tham số đầu vào là 42
.
Độc giả có thể đọc thêm bài trước về upgradeable contract của mình tại đây để hiểu rõ hơn về các khái niệm.
Hàm deployProxy
trông đơn giản nhưng thực tế nó thực hiện rất nhiều nhiệm vụ, quan trọng trong số đó là:
- Check xem implementation đã tồn tại - tức đã được deploy hay chưa? ta có thể thắc mắc rằng check bằng cách nào? check off-chain hay on-chain, bằng so sánh bytecode hay bằng cách nào? ở đây thì plugin chỉ đơn giản là check off-chain trong project, tức nếu contract đó đã từng được deploy trong project rồi thì nó sẽ được gắn một flag là đã đươc deploy, và không cần phải deploy lại nữa.
- Nếu implementation chưa tồn tại thì tạo một transaction deploy implementation (
tx1
) - Admin của proxy sẽ là một ownable contract tên là
ProxyAdmin
. Tương ứng với mỗi account trong project sẽ có mộtProxyAdmin
cho tất cả các proxy được tạo ra bởi account đó. Khi cần gọi các hàm sử dụng admin của proxy nhưupgradeTo
hayupgradeAndCall
, ta sẽ gọi nó thông qua hàm trongProxyAdmin
, các hàm này sẽ trigger chúng, và đương nhiên chỉ có owner củaProxyAdmin
mới được gọi mà thôi. - Tiến hành check xem
ProxyAdmin
đã tồn tại hay chưa? tương tự trên việc check này thực hiện off-chain trong project mà thôi. - Nếu
ProxyAdmin
chưa tồn tại thì tạo một transaction deployProxyAdmin
(tx2
) - Tiến hành deploy proxy sử dụng contract factory
TransparentUpgradeableProxy
(tx3
). Địa chỉ của implementation được lưu trữ vào_IMPLEMENTATION_SLOT
làkeccak256(eip1967.proxy.implementation) - 1
, hay0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
. Địa chỉ củaProxyAdmin
được lưu trữ vào_ADMIN_SLOT
làkeccak256(eip1967.proxy.implementation) - 1
, hay0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
.
Chạy script deploy:
npx hardhat run scripts/deployUpgradeableBox.js --network sepolia
> Deploying Box...
> Box deployed to: 0xa15eCE9a7049227e119e8216A1F2a04b9Eb617fC
ta nhận được địa chỉ của proxy. Khuyến khích độc giả hãy tự kiểm tra trên etherscan và terminal kết quả của các nhiệm vụ mình kể bên trên để có thể hiểu rõ hơn.
ví dụ kiểm tra địa chỉ của ProxyAdmin
:
cast storage 0xa15eCE9a7049227e119e8216A1F2a04b9Eb617fC 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 --rpc-url https://sepolia.infura.io/v3/<infura_id_here>
0x0000000000000000000000007a21711628e9396f0f04ca3d8853f308b8687274
kiểm tra địa chỉ của implementation:
cast storage 0xa15eCE9a7049227e119e8216A1F2a04b9Eb617fC 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url https://sepolia.infura.io/v3/<infura_id_here>
0x000000000000000000000000d40610a05f7c7e107b3ffff157c67c2c92b72f1b
2. Tiến hành upgrade contract
Ta tạo contract BoxV2.sol
có thêm một hàm mới increment
// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BoxV2 {
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
// Stores a new value in the contract
function store(uint256 newValue) public {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
function increment() public {
value = value + 1;
emit ValueChanged(value);
}
}
Viết script upgradeBox.js
để tiến hành upgrade:
// scripts/upgradeBox.js
const { ethers, upgrades } = require("hardhat");
async function main() {
const BoxV2 = await ethers.getContractFactory("BoxV2");
console.log("Upgrading Box...");
await upgrades.upgradeProxy(
"0xa15eCE9a7049227e119e8216A1F2a04b9Eb617fC", // proxy address here
BoxV2
);
console.log("Box upgraded");
}
main();
hàm upgradeProxy
thực hiện các công việc:
- kiểm tra off-chain implementation mới đã được deploy chưa (tương tự lúc trước), sau đó tiến hành deploy implementation mới nếu cần, ở đây là contract
BoxV2
. - gọi hàm
upgrade(address proxy, address implementation)
từ ProxyAdmin để tiến hành upgrade implementation của proxy sang địa chỉ của contractBoxV2
.
Chạy script:
npx hardhat run scripts/upgradeBox.js --network sepolia
> Upgrading Box...
> Box upgraded
Độc giả hãy tự kiểm tra lại các kết quả trên etherscan và terminal.
3. Check code phân quyền trong Transparent Upgradeable Proxy
Ta sẽ tự kiểm tra lại lý thuyết về phân quyền của transparent proxy patten trong bài trước trong code xem sao.
- khi người gọi là proxy admin: sẽ gọi trực tiếp hàm bên trong proxy nếu hàm đó tồn tại, và nếu hàm không tồn tại thì revert.
- khi người gọi không phải là proxy admin: thực hiện delegatecall sang logic contract
TransparentUpgradeableProxy.sol
function _fallback() internal virtual override {
if (msg.sender == _getAdmin()) {
bytes memory ret;
bytes4 selector = msg.sig;
if (selector == ITransparentUpgradeableProxy.upgradeTo.selector) {
ret = _dispatchUpgradeTo();
} else if (selector == ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
ret = _dispatchUpgradeToAndCall();
} else if (selector == ITransparentUpgradeableProxy.changeAdmin.selector) {
ret = _dispatchChangeAdmin();
} else if (selector == ITransparentUpgradeableProxy.admin.selector) {
ret = _dispatchAdmin();
} else if (selector == ITransparentUpgradeableProxy.implementation.selector) {
ret = _dispatchImplementation();
} else {
revert("TransparentUpgradeableProxy: admin cannot fallback to proxy target");
}
assembly {
return(add(ret, 0x20), mload(ret))
}
} else {
super._fallback();
}
}
- được kế thừa từ
Proxy.sol
/**
* @dev Delegates the current call to the address returned by `_implementation()`.
*
* This function does not return to its internal call site, it will return directly to the external caller.
*/
function _fallback() internal virtual {
_beforeFallback();
_delegate(_implementation());
}
/**
* @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other
* function in the contract matches the call data.
*/
fallback() external payable virtual {
_fallback();
}
/**
* @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data
* is empty.
*/
receive() external payable virtual {
_fallback();
}
/**
* @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback`
* call, or as part of the Solidity `fallback` or `receive` functions.
*
* If overridden should call `super._beforeFallback()`.
*/
function _beforeFallback() internal virtual {}
- admin ở đây chính là
ProxyAdmin.sol
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.3) (proxy/transparent/ProxyAdmin.sol)
pragma solidity ^0.8.0;
import "./TransparentUpgradeableProxy.sol";
import "../../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(ITransparentUpgradeableProxy 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(ITransparentUpgradeableProxy 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(ITransparentUpgradeableProxy 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(ITransparentUpgradeableProxy 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(
ITransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}