Всем привет.

«Можно ли изменить код смарт‑контракта и разместить по его по старому адресу?» — такой вопрос мне задали на собеседовании Solidity разработчика.

Точный ответ требует разбора вопроса и определения требований к задаче. Требование — «изменить контракт без изменения адреса». Для этого есть подходы с обновляемым смарт‑контрактом… Но оказалось, что нет. Это вопрос на знание опкодов EVM.

Сейчас расскажу как создатьуничтожитьразместить_иной_контракт по однму и тому же адресу смарт‑контракта (далее — СмК) без использования паттерна Transparent Proxy и UUPS.

Немного PR и маркетинга

Меня зовут Илья Дружинин, я инженер‑разработчик. Я из R&D команды web3 разработчиков. Мы исследуем и разрабатываем нетривиальные web3 проекты (оракулы, zkp, страхование, мосты и т. д.), не только для блокчейна Ethereum.

Также проводим персональное и командное обучение стеку технологий Web3 по программам различной длительности. По разработке, исследованиям и обсучению пишите https://t.me/didexbot.

Термины

op_code (оп_коды) — инструкция для виртуальной машины Ethereum. Инструкции выполняют набор последовательных действий с данными.

nonce — порядковый номер, в нашем случае транзакций.

Теория

Для размещения СмК в EVM используются оп_коды CREATE и CREATE2. Чаще всего используется оп_код CREATE, так как у него нет ограничения на то, кто его может вызвать.

CREATE

На уровне Solidity для создания СмК с помощью оп_кода CREATE используется ключевое слово new, без спецификатора salt.

contract D {
    uint public x;
    constructor(uint a) payable {
        x = a;
    }
}

contract C {
    D d = new D(4);
    
    function createD(uint arg) public {
        D newD = new D(arg);
    }
}

При этом адрес создаваемого СмК вычисляется на основе информации о том, кто сделал вызов и его nonce.

new_address = hash(sender, nonce)

У каждого аккаунта есть свой nonce: для обычных аккаунтов он увеличивается при каждой транзакции, а для СмК — при каждом создании нового СмК. Nonce нельзя использовать повторно, нельзя запросить из кода и они всегда имеют транзитивную последовательность.

Довольно сложно предугадать какой будет адрес у СмК, так как заранее неизвестен nonce отправителя транзакции, тем более, если создатель СмК другой СмК. Конечно можно перебрать множество вариантов address + nonce, но мы не сможем с уверенностью сказать какой из них будет адресом СмК.

CREATE2

Оп_код CREATE2 был введен с EIP-1014 (ветка Константинополь). Основная идея введения этого оп_кода — детерминированное создание адреса СмК без привязки к действиям в будущем. Этот оп_код создавался для повышения производительности блокчейна, через каналы состояний.

С помощью CREATE2 предсказуемым способом генерируется адрес, для этого нужны: 

  • константа 0xFF, которая предотвращает коллизии с CREATE;

  • адрес отправителя;

  • соль - произвольные данные предоставляемые отправителем;

  • байткод размещаемого СмК.

new_address = hash(0xFF, sender, salt, bytecode)

CREATE2 гарантирует, что если отправитель когда‑либо развернет байткод с использованием CREATE2 и предоставленной соли, то СмК будет развёрнут по адресу new_address. Так как байткод включён в вычисление адреса, то можно легко проверить, что именно этот байткод создаёт этот адрес, а не какой‑то иной. Далее в примере будет разъяснено лучше.

Примечание: CREATE2 может быть вызван только из СмК. Невозможно использовать CREATE2 для развертывания СмК с адреса пользователя.

CREATE2 может быть использован в широком спектре сценариев использования. Предположим, что web3 сервис предлагает пользователям создавать благотворительные комапнии со сбором средств. У каждой благотворительной кампании будет своя казна, срок выполнения и получатель(и), который(ие) получит(ют) выплату по цели. Развертывание СмК для пользователей может стать довольно дорогостоящим для web3 сервиса, поэтому развертывание СмК по сбору средств в момент создания было бы непомерно дорогим и плохо масштабируемым. Однако с CREATE2 СмК по сбору средств можно использовать без развертывания самого СмК. Платформа может определить пороговую точку, когда СмК может быть развернут. Это устраняет необходимость в создании СмК на начальном этапе, но при этом сохраняется возможность пополнения счета СмК и доверия к базовой логике.

Существуют некоторые особенности в отношении создания с помощью CREATE2. СмК может быть повторно создан по тому же адресу после уничтожения. Тем не менее, возможно, что вновь созданный СмК будет иметь другой развернутый байткод, даже если байткод создания был тем же самым (это является требованием, поскольку в противном случае адрес изменился бы). Это связано с тем, что конструктор может запросить внешнее состояние, которое могло измениться между двумя созданиями, и включить его в развернутый байткод до его сохранения.

А можем ли мы по одному и тому же адресу разместить разные СмК?

Да, можем, и вот пример.

Пример

Полный исходный код примера лежит тут: https://github.com/druzhtech/eth_dev/blob/main/contracts/contracts/Create2.sol 

Сперва повтор теории. Так как мы уже знаем, что адреса СмК создаются (op_code CREATE) c использованием адреса отправителя и его nonce, то нам нужно придумать механизм обнуления nonce.

Как обнулить nonce? Уничтожить СмК создающий СмК и создать на его место такой же СмК. 

Алгоритм

  1. Мы создаём Фабрику СмК с помощью CREATE2

    1. Впоследствии мы сможем снова по этому адресу разместить код Фабрики.

  2. Создаём с помощью Фабрики СмК_А

    1. nonce адреса Фабрики становится 1

  3. Уничтожаем СмК_А

  4. Уничтожаем Фабрику

    1. nonce адреса Фабрики становятся 0.

  5. Снова создаём эту же Фабрику с помощью CREATE2

  6. Создаём с помощью Фабрики СмК_Б 

    1. адрес СмК_Б будет такой же как у СмК_А, но код разный.

Код

В основном СмК создаём функцию создания Фабрики СмК, используя CREATE2. 

На уровне языка Solidity мы можем использовать ключевое слово new и специфицировать имя создаваемого СмК конструкцией {salt: salt}. Так EVM создаст СмК с помощью оп_кода CREATE2

function deploy() external {
  
  address addr = address(new ContractFactory{salt: salt}()); // create2
  
  emit ContractDeployed(addr);
}

В СмК Фабрики (ContractFactory) создать три функции

function deployContract1() external {
  address addr = address(new ContractV1()); // create
  emit ContractDeployed(addr);
}

function deployContract2() external {
  address addr = address(new ContractV2()); // create
  emit ContractDeployed(addr);
}

function destroy() external {
  selfdestruct(payable(creator));
}

СмК ContractV1 

contract ContractV1 {
  address creator;
  
  constructor() {
    creator = msg.sender;
  }

  function withdraw() external {
    (bool res, ) = creator.call{value: address(this).balance}("");
    require(res, "failed");
  }
  
  function destroy() external {
    selfdestruct(payable(creator));
  }
}

СмК ContractV2 основываем на ContractV1, но меняем функцию withdraw

function withdraw() external {
  (bool res,) = address(0xaB854be0A4d499B6FD8D0bB5F796Ab5b33cE825b).call{value: address(this).balance}("");
  require(res, "failed");
}

Теперь, пользователь проверяя СмК по адресу с которым ранее работал, видит не изменённую сигнатуру функции withdraw, но поведение её уже изменено.

Данные смарт‑контракта, естественно, уничтожены. Но пользователь может отправить ether и потерять его.

После сказанного

Команда selfdestruct c версии 0.8.19 помечается как устаревшая и не рекомендованная к использованию. Напомню, у неё есть несколько особенных поведений:

  • пополнение любого адреса эфирками, даже если адрес не хочет принимать эфирки

  • тотальная аннигиляция ether: selfdestruct(address(this))

Источники

Комментарии (0)