Skip to content

Thực hành với Upgradeable Contract

Posted on:February 3, 2023

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à:

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:

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
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();
    }
}
/**
 * @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 {}
// 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);
    }
}

4. Tham khảo