Проект PetShop, день 3: Обновление PetShop NFT

На третий день я собираюсь обновить мой PetShop NFT до версии 2.

Создание PetShop версии 2

В PetShop версии 2 я собираюсь внести 2 небольших изменения:

  • В методе mintToken() я буду вызывать _safeMint() вместо _mint(). Метод _safeMint() выполняет несколько дополнительных проверок перед майнингом NFT: ID токена не должен существовать, и если принимающий адрес является смарт-контрактом, он должен реализовать интерфейс IERC721Receiver.
  • Я добавлю новый внешний метод version(), который возвращает номер текущей версии контракта.

Чтобы понять, как работает обновляемый контракт, нам нужно знать шаблон обновления прокси, используемый библиотекой обновляемых контрактов OpenZeppelin.

Когда мы развернули нашу первую версию контракта PetShop NFT, мы фактически развернули прокси-контракт и логический контракт:

  • Прокси-контракт хранит адрес логического контракта. Он делегирует все вызовы функций логическому контракту.
  • Функция логического контракта будет выполняться в контексте состояния прокси-контракта. Это означает, что все переменные состояния, объявленные в логическом контракте, фактически хранятся в хранилище прокси.

Очень важно понимать это разделение кода выполнения и переменных состояния.

В Solidity код, который находится внутри конструктора или является частью объявления глобальной переменной, не является частью байткода выполнения развернутого контракта. Этот код выполняется только один раз, когда экземпляр контракта развертывается. Как следствие этого, код в конструкторе логического контракта никогда не будет выполняться в контексте состояния прокси. Перефразируя, можно сказать, что прокси полностью забывают о существовании конструкторов. Это просто как если бы их не было для прокси.

Чтобы решить эту проблему, логические контракты должны перенести код конструктора в обычную функцию «инициализатор», и чтобы эта функция вызывалась всякий раз, когда прокси связывается с этим логическим контрактом. Особое внимание следует уделить функции инициализатора, чтобы она могла быть вызвана только один раз, что является одним из свойств конструкторов в общем программировании.

Для каждой новой версии контракта мы должны предоставлять функцию «реинициализатор», чтобы сделать все необходимое для перехода от предыдущей версии. Эта функция «reinitializer» должна вызываться как часть процесса обновления и отключаться по его завершении.

Вот наш PetShopV2.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol";
import "hardhat/console.sol";

contract PetShopV2 is ERC721URIStorageUpgradeable {
    using CountersUpgradeable for CountersUpgradeable.Counter;
    CountersUpgradeable.Counter private tokenIds;

    uint8 private constant VERSION = 2;

    // The "reinitializer" function. It does nothing interesting, though.
    // I added this function to demonstrate a complete upgrade process.
    function initializeV2() reinitializer(VERSION) public {
        console.log("Initializing PetShop version %s...", VERSION);
    }

    function mintToken(string calldata _tokenURI, address _to) external returns (uint256) {
        tokenIds.increment();
        uint256 newTokenId = tokenIds.current();
        _safeMint(_to, newTokenId);
        _setTokenURI(newTokenId, _tokenURI);
        return newTokenId;
    }

    function version() external pure returns (uint8) {
        return VERSION;
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мы используем модификатор reinitializer (из OpenZeppelin’s Initializable), который гарантирует, что функция «reinitializer» может быть вызвана не более одного раза, и только если контракт не был инициализирован до большей версии ранее.

В нашем тесте мы обновляем deployPetShopFixture(), чтобы подготовить PetShop версии 2.

async function deployPetShopFixture() {
  const PetShopV1 = await ethers.getContractFactory("PetShop");
  const petShopV1 = await upgrades.deployProxy(PetShopV1);
  await petShopV1.deployed();
  const proxyAddress = petShopV1.address;

  const PetShopV2 = await ethers.getContractFactory("PetShopV2");
  const petShopV2 = await upgrades.upgradeProxy(proxyAddress, PetShopV2);
  // Call the "reinitializer" function of this new version.
  await petShopV2.initializeV2();
  console.assert(petShopV2.address === proxyAddress, "Proxy address should not change.");

  const accounts = await ethers.getSigners();

  return {
    PetShop: PetShopV2,
    petShop: petShopV2,
    accounts: accounts,
  };
}
Вход в полноэкранный режим Выход из полноэкранного режима

Старые тестовые случаи должны пройти. Кроме того, я добавлю дополнительный тест для метода version():

describe("Deployment", function() {
  // ...
  it("should upgrade proxy to version 2", async function() {
    const { petShop } = await loadFixture(deployPetShopFixture);
    expect(await petShop.version()).to.equal(2);
  });
});
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь запустите тест:

$ npx hardhat test
  PetShop contract
    Deployment
      ✔ should initialize the NFT name and symbol (2425ms)
      ✔ should upgrade proxy to version 2
    Transactions
      ✔ should mint NFTs (206ms)
Войти в полноэкранный режим Выйти из полноэкранного режима

Создайте задачу для обновления PetShop NFT

Теперь создайте задачу для обновления PetShop NFT до версии 2:

task("petshop-upgrade-v2", "Upgrades PetShop NFT to version 2")
  .addParam("address", "The contract address")
  .setAction(async (taskArgs) => {
    const [deployer] = await ethers.getSigners();
    console.log(`Deployer: ${deployer.address} (balance: ${await deployer.getBalance()})`);

    // See: https://docs.openzeppelin.com/upgrades-plugins/1.x/hardhat-upgrades
    console.log(`Upgrading proxy contract (${taskArgs.address}) to version 2...`);
    const PetShopV2 = await ethers.getContractFactory("PetShopV2");
    const petShopV2 = await upgrades.upgradeProxy(taskArgs.address, PetShopV2);
    await petShopV2.deployed();
    console.assert(petShopV2.address === taskArgs.address, "Proxy contract address should not change.");

    // Call the reinitializer function.
    await petShopV2.initializeV2();

    const name = await petShopV2.name();
    const symbol = await petShopV2.symbol();
    const version = await petShopV2.version();
    console.log(`Upgraded contract ${name} (symbol: ${symbol}) to version ${version}.`);
  });
Войти в полноэкранный режим Выйти из полноэкранного режима

Запустите локальный сетевой демон Hardhat. Затем в другом терминале попробуйте развернуть контракт версии 1, а затем обновить его до версии 2:

$ npx hardhat petshop-deploy --network localhost
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (balance: 10000000000000000000000)
Deployed PetShop at: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Querying NFT: name = Pet Shop; symbol = PET

$ npx hardhat petshop-upgrade-v2 --network localhost 
    --address 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (balance: 9999996884701105402650)
Upgrading proxy contract (0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0) to: PetShopV2
Upgraded contract Pet Shop (symbol: PET) to version 2.
Войдите в полноэкранный режим Выйти из полноэкранного режима

Обновление PetShop NFT в сети Goerli testnet

Ранее у меня уже был развернут PetShop NFT в Goerli testnet. Его адрес 0xff27228e6871eaB08CD0a14C8098191279040c13, его можно посмотреть на Etherscan. Теперь запустим задачу petshop-upgrade-v2 для его обновления:

$ npx hardhat petshop-upgrade-v2 --network goerli 
    --address 0xff27228e6871eaB08CD0a14C8098191279040c13
Deployer: 0xCc4c8184CC4A5A03babC13D832cEE3E41bE92d08 (balance: 735936919700656242)
Upgrading proxy contract (0xff27228e6871eaB08CD0a14C8098191279040c13) to: PetShopV2
Upgraded contract Pet Shop (symbol: PET) to version 2.
Войдите в полноэкранный режим Выйти из полноэкранного режима

После обновления на Etherscan мы видим, что было выдано событие Upgraded:

Обратите внимание, что адрес прокси-контракта не меняется во время обновления. Мы просто создадим новый логический контракт и обновим прокси-контракт, чтобы он указывал на него. Поскольку прокси-контракт — это тот контракт, с которым взаимодействуют пользователи или кошельки, такое обновление прозрачно для держателей токенов.

Заключение

Это мой третий день работы в Ethereum. Изменения в PetShop NFT являются лишь незначительными. Важнее понять, как управлять обновлением смарт-контрактов, используя шаблон обновления прокси OpenZeppelin. Полный исходный код можно найти здесь: https://github.com/zhengzhong/petshop/releases/tag/day03.

Ссылки

  • Написание обновляемых контрактов
  • Шаблон обновления прокси

Оцените статью
devanswers.ru
Добавить комментарий