Solidity
April 4, 2023

Solidity | Паттерны 

Привет. Сегодня пойдет речь о паттернах в solidity. Расскажу о самых популярных и нужных на мой взгляд. Реализуем несколько из них.

commit/reveal

Так как блокчейн имеет такую особенность, что все данные из СК, которые хранятся в нем, лежат в открытом доступе и каждый может их посмотреть(даже если они pivate), а мы например хотим скрыть какую то информацию от глаз, то к нам на помощь идет commit/reveal. Но как? При помощи него мы будем хэшировать информацию с какими то данными так, чтоб ее было не возможно расшифровать. После чего, только мы создатели информации сможем ее дешифровать.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract commitReveal{    
    mapping (address => bytes32) commits;
    function commit(bytes32 _hash) public {        
        require(commits[msg.sender] == bytes32(0));        
        commits[msg.sender] = _hash;    
    }
    function reveal(string calldata frase) public view{        
        bytes32 isCommit = keccak256(abi.encodePacked(frase, msg.sender));        
        require(isCommit == commits[msg.sender], "Faild");    
    }
}

Мы создаем маппинг, где будем хранить информацию по адресу в bytes32. Bytes32, потому что это хэш информации.

Как будет образовываться этот хэш?

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

Пример кодирования информации во фронтенде:

Это утилита из ethers.js, которая поможет нам создать хэш из строки и адреса: ethers.utils.solidityKeccak256(['string', 'address'], ['Hello world', '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4'])
Выходной хэш, после передачи аргументов:
0x33dda2f3a831ef0742bd6c7266faa711e99198163a0c898705ed16ec9f124101

Таким образом мы незаметно от всех создали хэш наших данных.

После чего мы передаем его в нашу функцию commit, где идет запись в маппинг.

Дальше есть функция reveal, где мы на вход аргумента функции передаем строку и в теле функции кодируем хэш, состоящий из наших аргументов, дальше проверяем совпал он с тем хэшем, который мы кодировали во фронте или нет.

Важно! Последовательность аргументов должна совпадать как на фронте, так и в функции ск.

Этот паттерн можно использовать для анонимного голосования или анонимного опросника например. В обще используем.

timeLock + multiSgn

Следующий паттерн, о котором пойдет речь, это timeLock. Он нужен для того, чтоб поставить в очередь на выполнение транзакции. А multiSgn для того, чтоб выполнение транзакции было только в том случае, когда количество голосов за выполнение будет достаточно, иначе транзакция не выполнится.

 // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract timeLock {  
    uint constant MIN_DELAY = 10;    
    uint constant MAX_DELAY = 5 days;    
    uint constant PERIOD = 2 days; 
    uint public constant CONFIRMATIONS = 3;     
    address[] public owners;     
    mapping (bytes32 => bool) queue;    
    mapping(address => bool) public isOwner;             
    mapping(bytes32 => uint) public confirmationsTxAmount;    
    mapping(bytes32 => mapping(address => bool)) public confirmations;
    
    modifier onlyOwner() {        
        require(isOwner[msg.sender], "not an owner!");        
        _;    
    }    
    constructor(address[] memory _owners){         
        require(_owners.length >= CONFIRMATIONS, "not enough owners!");
        for(uint i = 0; i < _owners.length; i++) {            
            address nextOwner = _owners[i];
            require(nextOwner != address(0), "zero address");            
            require(!isOwner[nextOwner], "already owner!");
            isOwner[nextOwner] = true;            
            owners.push(nextOwner);        
        }    
    }
    function add2Q(        
      address _to,        
      string calldata _func,        
      bytes calldata _data,        
      uint _value,        
      uint _timestamp    
    ) public onlyOwner returns(bytes32){
        require(_timestamp > block.timestamp + MIN_DELAY 
        && _timestamp < block.timestamp + MAX_DELAY, "Faild");        
        bytes32 id = keccak256(abi.encode(            
          _to,            
          _func,            
          _data,            
          _value,            
          _timestamp        
        ));        
        require(!queue[id], "Faild!");        
        queue[id] = true;        
        return id;    
    }
    function confirm(bytes32 id) external onlyOwner {        
        require(queue[id], "not queued!");        
        require(!confirmations[id][msg.sender], "already confirmed!");
        confirmationsTxAmount[id]++;        
        confirmations[id][msg.sender] = true;    
    }
    function extract(bytes32 id) public onlyOwner{        
        require(queue[id], "Faild");        
        delete queue[id];        
        delete confirmationsTxAmount[id];        
        for(uint i = 0; i < owners.length; i++) {            
            delete confirmations[id][owners[i]];        
        }    
    }
    function exe(        
      address _to,        
      string calldata _func,        
      bytes calldata _data,        
      uint _value,        
      uint _timestamp    
    ) public payable onlyOwner{                 
    require(_timestamp < block.timestamp |
    | _timestamp + PERIOD > block.timestamp, "Error period!");               bytes32 id = keccak256(abi.encode(            _to,            _func,            _data,            _value,            _timestamp        ));                require(queue[id], "not queued!");       
        require(confirmationsTxAmount[id] >= CONFIRMATIONS, "not enough!");        delete queue[id];        delete confirmationsTxAmount[id];        for(uint i = 0; i < owners.length; i++) {            delete confirmations[id][owners[i]];        }        bytes memory data;        if(bytes(_func).length > 0) {            data = abi.encodePacked(                bytes4(keccak256(bytes(_func))),                _data            );        } else {            data = _data;        }
        (bool success, ) = _to.call{value: _value}(data);        
        require(success, "error trans!");
    }   
}

mapping

У нас есть целых 4 маппинга

queue нужен для добавление в очередь транзакции по id или хэшу, о нем позже

isOwner нужен для проверки onlyOwner, так как у нас будет массив овнеров, об этом дальше пойдет речь.

confirmationsTxAmount нужен для того, чтоб считать количество голосов за ту или иную транзакцию

confirmations сложный маппинг, где мы будем проверять, что на такую то транзакцию такой то адрес отдал голос.

Дальше поймете их смысл.

Конструктор.

constructor(address[] memory _owners){         
        require(_owners.length >= CONFIRMATIONS, "not enough owners!");
        for(uint i = 0; i < _owners.length; i++) {            
            address nextOwner = _owners[i];
            require(nextOwner != address(0), "zero address");            
            require(!isOwner[nextOwner], "already owner!");
            isOwner[nextOwner] = true;            
            owners.push(nextOwner);        
        }    
    }

Так как у нас система мульти подписи, то нужно объявить массив овнеров address[] public owners, для того, чтоб его заполнить, мы должны в конструктор передать все адреса кошельков, которые будут овнерами, это аргумент конструктора. После чего мы делаем ряд проверок, на то что не было получено 2 одинаковых адреса и нету нулевых адресов, и заполняем массив циклом for.

Функция add2Q

function add2Q(        
      address _to,        
      string calldata _func,        
      bytes calldata _data,        
      uint _value,        
      uint _timestamp    
    ) public onlyOwner returns(bytes32){
        require(_timestamp > block.timestamp + MIN_DELAY 
        && _timestamp < block.timestamp + MAX_DELAY, "Faild");        
        bytes32 id = keccak256(abi.encode(            
          _to,            
          _func,            
          _data,            
          _value,            
          _timestamp        
        ));        
        require(!queue[id], "Faild!");        
        queue[id] = true;        
        return id;    
}

Эта функция и будет ставить в очередь транзакцию. Передаем в аргументы функции все данные о транзакции

address _to - адрес где находиться функция          
strging  _func - название функции          
 bytes _data - данные, например сообщение        
 uint _value - деньги      
 uint _timestamp - метка времени 

Дальше проверяем, что наша метка времени начала очереди входит в диапазон, который мы ввели. Тут я объявляю переменные constant в начале СК, которые нельзя изменить. Диапазон нужен, чтоб не делать максимально тупые запросы например выполнение транзакции через 100 лет.

Дальше

 bytes32 id = keccak256(abi.encode(            
 _to,            
 _func,            
 _data,            
 _value,           
 _timestamp       
 ));

Вот эта непонятная штука просто создает хэш нашей транзакции, со всеми данными или id транзакции если понятнее. Как в прошлом паттерне.

Потом мы проверяем, что такая транзакция не существует в очереди и добавляем ее туда.

Функция confirm

function confirm(bytes32 id) external onlyOwner {        
        require(queue[id], "not queued!");        
        require(!confirmations[id][msg.sender], "already confirmed!");
        confirmationsTxAmount[id]++;        
        confirmations[id][msg.sender] = true;    
    }

Нужна для того, чтоб наши овнеры СК подтверждали свой голос за транзакцию, которую мы передаем как аргумент. Делаем две проверки на то, что такая транзакция есть и на то, что мы еще не голосовали за эту транзакцию (как раз тот сложный маппинг). Добавляем голос к общему голосу за выполнение этой транзакции confirmationsTxAmount[id]++, и говорим, что мы проголосовали: confirmations[id][msg.sender] = true

Функция extract

function extract(bytes32 id) public onlyOwner{        
        require(queue[id], "Faild");        
        delete queue[id];        
        delete confirmationsTxAmount[id];        
        for(uint i = 0; i < owners.length; i++) {            
            delete confirmations[id][owners[i]];        
        }    
    }

Нужна, если в итоге мы либо не набрали нужное количество голосов или прошло время выполнения.

Все что нам нужно сделать, это проверку на то, что данная транзакция существует в очереди и удалить ее из всех маппингов. Замечу, что для удаления из сложного маппинга, нам придется идти по циклу и обращаться к каждому адресу из овнеров.

Функция exe

 function exe(        
      address _to,        
      string calldata _func,        
      bytes calldata _data,        
      uint _value,        
      uint _timestamp    
    ) public payable onlyOwner{                 
    require(_timestamp < block.timestamp |
    | _timestamp + PERIOD > block.timestamp, "Error period!");               bytes32 id = keccak256(abi.encode(            _to,            _func,            _data,            _value,            _timestamp        ));                require(queue[id], "not queued!");       
        require(confirmationsTxAmount[id] >= CONFIRMATIONS, "not enough!");        delete queue[id];        delete confirmationsTxAmount[id];        for(uint i = 0; i < owners.length; i++) {            delete confirmations[id][owners[i]];        }        bytes memory data;        if(bytes(_func).length > 0) {            data = abi.encodePacked(                bytes4(keccak256(bytes(_func))),                _data            );        } else {            data = _data;        }
        (bool success, ) = _to.call{value: _value}(data);        
        require(success, "error trans!");
    }   

Если мы решили выполнить транзакцию, то мы опять передаем все аргументы для вторичной проверки, но можно передать просто хэш, как и в других функциях. Проверяем, что мы еще не просрочили наше время. Создаем снова id и проверяем на существование этой транзакции в очереди. Так же смотрим, чтоб у нас было достаточно голосов для подтверждения. Потом удаляем из всех маппингов как в extract, потому что они нам больше не нужны и делаем важную вещь:

 bytes memory data;        
 if(bytes(_func).length > 0) {            
     data = abi.encodePacked(                
       bytes4(keccak256(bytes(_func))),                
       _data            
     );        
 } else {           
   data = _data;        
 }
 (bool success, ) = _to.call{value: _value}(data);        
 require(success, "error trans!");

Этот момент из прошлой статьи. Для вызова функции мы будет использовать низкоуровневый вызов. Проверка нужна для того, чтоб узнать существует ли селектор функции, или мы хотим вызвать функцию fallback, но в целом эта проверка нужна, для безопасности, ведь мы в всегда будем передавать func. Так как селектор функции это первые 4 байта, то мы должны их получить.

bytes4(keccak256(bytes(_func))) В данный момент мы просто берем функцию как в низкоуровневых вызовах string _func = "demo(string)" например. После хэшируем и берем 4 первые 4 байта, это и есть тот селектор.

Ну в целом и весь паттерн готов. Он может показаться сложным на первый взгляд, но если самому начать его проверять, то станет все понятно. Все паттерны будут на гитхабе если что.

governance

Этот паттерн реализован в опензеплине, и если хотите посмотреть как он работает полностью, то вам туда. Я же расскажу про его принцип и малую часть. Потому что там инфы на отдельную статью.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./IVotes.sol";
contract Governor {    
    struct ProposalVote {        
        uint againstVotes;        
        uint forVotes;        
        uint abstainVotes;        
        mapping(address => bool) hasVoted;    
    }
    struct Proposal {        
        uint votingStarts;        
        uint votingEnds;        
        bool executed;        
        bool canceled;    
    }
    
    mapping(uint256 => Proposal) public proposals;    
    mapping(uint256 => ProposalVote) public proposalVotes;

    enum VoteType { Against, For, Abstain }   
    enum ProposalState { Pending, Active, Succeeded, Defeated, Execute, Cancele }
    
    uint public constant VOTING_DELAY = 100;    
    uint public constant VOTING_DURATION = 500;
    
      
    IVotes public immutable token;
    
    constructor(IVotes tokenAddress) {        
        token = tokenAddress;    
    }        
    function _getVotes(
      address account, 
      uint256 blockNumber, 
      bytes memory
    ) internal view virtual  returns (uint256) {        
    return token.getPastVotes(account, blockNumber);    
    }    
    function propose(        
      address targets,        
      uint values,        
      bytes calldata calldatas,        
      string calldata description    
    ) external returns(uint256) {        
        require(            
          _getVotes(msg.sender, block.number - 1, "") >= 0,            
          "Governor: proposer votes not anougth"        
        );
        uint256 proposalId = hashProposal(            
          targets, 
          values, 
          calldatas, 
          keccak256(bytes(description))        
        );
        require(proposals[proposalId].votingStarts == 0, "proposal already exists");
        proposals[proposalId] = Proposal({            
          votingStarts: block.timestamp + VOTING_DELAY,            
          votingEnds: block.timestamp + VOTING_DELAY + VOTING_DURATION,            
          executed: false,            
          canceled: false        
        });
        
        
        
        return proposalId;    
    }   
    function execute(        
      address targets,        
      uint values,        
      bytes calldata calldatas,        
      string calldata description    
    ) external returns(bytes memory) {        
        uint256 proposalId = hashProposal(            
          targets, 
          values,
          calldatas, 
          keccak256(bytes(description))        
        );
        require(state(proposalId) == ProposalState.Succeeded, "invalid state");
        proposals[proposalId].executed = true;
        (bool success, bytes memory resp) = targets.call{value: values}(calldatas);        
        require(success, "tx failed");
        return resp;    
    } 
    function _cancel(        
      address targets,        
      uint values,        
      bytes calldata calldatas,        
      string calldata description    
    ) internal virtual returns (uint256) {        
        uint256 proposalId = hashProposal(           
          targets, 
          values, 
          calldatas, 
          keccak256(bytes(description))        
        );
        ProposalState status = state(proposalId);
        require(            
          status == ProposalState.Defeated,            
          "proposal not active"        
        );        
        proposals[proposalId].canceled = true;
        return proposalId;    
    }     
    function _countVote(        
      uint256 proposalId,        
      address account,        
      uint8 support,        
      uint256 weight,        
      bytes memory    
    ) internal virtual  {        
        ProposalVote storage proposalVote = proposalVotes[proposalId];
        require(!proposalVote.hasVoted[account], "GovernorVotingSimple: vote already cast");       
        require(state(proposalId) == ProposalState.Active, "vote not currently active");
        uint256 weight = _getVotes(account, proposal.votingStarts, params);        
        if (support == uint8(VoteType.Against)) {            
            proposalVote.againstVotes += weight;        
        } else if (support == uint8(VoteType.For)) {            
            proposalVote.forVotes += weight;        
        } else if (support == uint8(VoteType.Abstain)) {            
            proposalVote.abstainVotes += weight;        
        } else {            
            revert("invalid value for enum VoteType");        
        }        
        proposalVote.hasVoted[account] = true;    
    }
    function state(uint proposalId) public view returns (ProposalState) {        
        Proposal storage proposal = proposals[proposalId];        
        ProposalVote storage proposalVote = proposalVotes[proposalId];
        require(proposal.votingStarts > 0, "proposal doesnt exist");
        if (proposal.executed) {            
            return ProposalState.Execute;        
        }
        if (proposal.canceled) {            
            return ProposalState.Cancele;        
        }
        if (block.timestamp < proposal.votingStarts) {            
            return ProposalState.Pending;        
        }
        if(block.timestamp >= proposal.votingStarts &&           
          proposal.votingEnds > block.timestamp) {            
            return ProposalState.Active;        
        }
        if(proposalVote.forVotes > proposalVote.againstVotes) {           
            return ProposalState.Succeeded;        
        } else {           
            return ProposalState.Defeated;        
        }    
    }
    function hashProposal(        
      address targets,        
      uint values,        
      bytes calldata calldatas,        
      bytes32 description    
    ) internal pure returns(uint256) {        
        return uint256(keccak256(abi.encode(            
          targets, 
          values, 
          calldatas, 
          description        
        )));   
    }
    receive() external payable {}
}

Принцип работы:

У нас есть смарт-контракт A, у которого есть функция для выполнения, но мы передаем управление этим смарт-контрактом, контракту B governance, который и будет выполнять обязанности овнера смарт-контракта A. Наш СК governance будет работать по принципу голосования. То есть если достаточно голосов за выполнение, то транзакция выполняется, если нет, то не выполняется

Обычно для того чтоб голосовать используют токены ERC20 или ERC721. Например с ERC20: если у вас больше токенов, ваш голос тяжелее, если меньше то слабее. И таким образом все владельцы токенов могут голосовать за выполнение транзакции.

Я буду использовать ERC20 токены для голосования, но нам нужно будет не просто ERC20 стандарт, а ERC20Votes. Там реализована система checkPoints, благодаря которой мы избавимся от абуза. А именно: если человек будет переводить токены из аккаунта на аккаунт и голосовать, то он сможет голосовать одними и теми же токенами много раз подряд.

Как реализовать ERC20Votes я не буду показывать, потому что это достаточно сложно и не по теме. Мы просто импортируем интерфейс и не будет парится.

Первое что нам нужно, это структуры:

  struct ProposalVote {        
        uint againstVotes;        
        uint forVotes;        
        uint abstainVotes;        
        mapping(address => bool) hasVoted;    
    }
    struct Proposal {        
        uint votingStarts;        
        uint votingEnds;        
        bool executed;        
        bool canceled;    
    }

ProposalVote нужна для подсчета голосов за, против, нейтрально.

Proposal нужна для установки начала конца и состояния предложения.

Так же нам нужны маппинги proposals и proposalVotes, потому что предложений может быть много. Ключом будет id предложения и значение наши структуры соответственно.

struct ProposalVote {        
        uint againstVotes;        
        uint forVotes;        
        uint abstainVotes;        
        mapping(address => bool) hasVoted;    
    }
    struct Proposal {        
        uint votingStarts;        
        uint votingEnds;        
        bool executed;        
        bool canceled;    
    }
    
    mapping(uint256 => Proposal) public proposals;    
    mapping(uint256 => ProposalVote) public proposalVotes;

Так же создадим свои типы данных enum о них по подробнее позже

enum VoteType { Against, For, Abstain }   
enum ProposalState { Pending, Active, Succeeded, Defeated, Execute, Cancele }
    

Приступим к написанию функций:

Конструктор, который нам нужен чтоб указать адрес токена, который мы будем использовать для голосования.

 IVotes public immutable token;
    
    constructor(IVotes tokenAddress) {        
        token = tokenAddress;    
    }  

Функция propose:

function propose(        
      address targets,        
      uint values,        
      bytes calldata calldatas,        
      string calldata description    
    ) external returns(uint256) {        
        require(            
          _getVotes(msg.sender, block.number - 1, "") >= 0,            
          "proposer votes not anougth"        
        );
        uint256 proposalId = hashProposal(            
          targets, 
          values, 
          calldatas, 
          keccak256(bytes(description))        
        );
        require(proposals[proposalId].votingStarts == 0, "proposal already exists");
        proposals[proposalId] = Proposal({            
          votingStarts: block.timestamp + VOTING_DELAY,            
          votingEnds: block.timestamp + VOTING_DELAY + VOTING_DURATION,            
          executed: false,            
          canceled: false        
        });
        
        
        
        return proposalId;    
    }   

Это функция создания предложения.

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

Дальше мы должны создать id нашего предложения на подобии предыдущего паттерна, только еще мы образуем его в число для удобства.

P.S можно было так же сделать и в предыдущем паттерне

Функцию hashProposal напишем позже.

Дальше мы проверяем существует ли уже такое предложение или нет. Проверка идет по маппингу proposals и смотрится есть ли начало этого предложение или нет.

И дальше мы просто указываем этому предложению наши параметры начала конца и статусы.

Функция hashProposal:

function hashProposal(        
      address targets,        
      uint values,        
      bytes calldata calldatas,        
      bytes32 description    
    ) internal pure returns(uint256) {        
        return uint256(keccak256(abi.encode(            
          targets, 
          values, 
          calldatas, 
          description        
        )));   
    }

Это функция хеширования транзакции. Все так же как прошлом паттерне, только в конце преобразуем в uint256

Функция _getVotes

function _getVotes(
      address account, 
      uint256 blockNumber, 
      bytes memory
    ) internal view virtual  returns (uint256) {        
    return token.getPastVotes(account, blockNumber);    
    } 

Тут мы используя интерфейс IVotes обращаемся к ERC20Votes и узнаем баланс токенов на аккаунте на момент данного блока.

Функция _countVote

    function _countVote(        
      uint256 proposalId,        
      address account,        
      uint8 support,        
      uint256 weight    
    ) internal virtual  { 
           
        ProposalVote storage proposalVote = proposalVotes[proposalId];
        require(!proposalVote.hasVoted[account], "vote already cast");
        require(state(proposalId) == ProposalState.Active, "vote not currently active");
        uint256 weight = _getVotes(account, block.number-1, "");
        if (support == uint8(VoteType.Against)) {            
            proposalVote.againstVotes += weight;        
        } else if (support == uint8(VoteType.For)) {            
            proposalVote.forVotes += weight;        
        } else if (support == uint8(VoteType.Abstain)) {            
            proposalVote.abstainVotes += weight;        
        } else {            
            revert("invalid value for enum VoteType");        
        }        
        proposalVote.hasVoted[account] = true;    
    }

Это функция добавления голосов.

Аргументы функции:

proposalId - id предложения,
account - адрес голосующего,
support - тип голоса (0 - за, 1 - против, 2 - нейтрально),
bytes - данные.

Дальше по id получаем структуру данных proposalVotes и делаем проверку: проголосовал ли данный аккаунт до этого или нет. Дальше смотрим баланс в данный момент у данного аккаунта и записываем в weight. Смотрим какой голос был получен и добавляем в proposalVotes его голос(weight). Ну и в конце указываем, что данный аккаунт проголосовал.

По идее можно использовать просто сравнение с 0,1,2, но в openzeppelin делают через enum, поэтому решил показать как там.

Функция state

function state(uint proposalId) public view returns (ProposalState) {        
        Proposal storage proposal = proposals[proposalId];        
        ProposalVote storage proposalVote = proposalVotes[proposalId];
        require(proposal.votingStarts > 0, "proposal doesnt exist");
        if (proposal.executed) {            
            return ProposalState.Execute;        
        }
        if (proposal.canceled) {            
            return ProposalState.Cancele;        
        }
        if (block.timestamp < proposal.votingStarts) {            
            return ProposalState.Pending;        
        }
        if(block.timestamp >= proposal.votingStarts &&           
          proposal.votingEnds > block.timestamp) {            
            return ProposalState.Active;        
        }
        if(proposalVote.forVotes > proposalVote.againstVotes) {           
            return ProposalState.Succeeded;        
        } else {           
            return ProposalState.Defeated;        
        }    
    }

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

Функция execute

function execute(        
      address targets,        
      uint values,        
      bytes calldata calldatas,        
      string calldata description    
    ) external returns(bytes memory) {        
        uint256 proposalId = hashProposal(            
          targets, 
          values,
          calldatas, 
          keccak256(bytes(description))        
        );
        require(state(proposalId) == ProposalState.Succeeded, "invalid state");
        proposals[proposalId].executed = true;
        (bool success, bytes memory resp) = targets.call{value: values}(calldatas);        
        require(success, "tx failed");
        return resp;    
    } 

Функция, которая вытворяет в жизнь предложение

Снова хэшируем данные, чтоб получить id и проверяем состояние нашего предложения. Нас устраивает только случай когда состояние предложения succeeded, иначе откат. Дальше мы устанавливаем значение true для executed и низкоуровневым вызовом вызываем функцию, как в предыдущем примере, только в calldatas мы передаем сразу и селектор функции и сообщение.

P.S тут можно вызывать как и в прошлом паттерне

Функция _cancel

function _cancel(        
      address targets,        
      uint values,        
      bytes calldata calldatas,        
      string calldata description    
    ) internal virtual returns (uint256) {        
        uint256 proposalId = hashProposal(           
          targets, 
          values, 
          calldatas, 
          keccak256(bytes(description))        
        );
        ProposalState status = state(proposalId);
        require(            
          status == ProposalState.Defeated,            
          "proposal not active"        
        );        
        proposals[proposalId].canceled = true;
        return proposalId;    
    }  

Тут все ровно так же как в execute, только проверка будет на Defeated и если она проходит, то мы установим соответственно, что предложение отменено.

Вот такой паттерн. Правда там в openzeppelin, реализация сложнее и для общего случая, но в основном этого достаточно.

Proxy

Этот паттерн для продвинутых.

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

Все мы знаем, что смарт-контракты, которые уже развернуты не могут быть изменены из за специфики блокчейна, но бывает ситуация когда нам нужно изменить что то в своем СК. И для этого придумали систему proxy.

Как это работает:

У нас есть СК, где реализована вся логика, но мы как пользователь будем обращаться не к самому СК, а к его Proxy, который будет хранить и унаследовать в себе все данные и переменные из нашего смарт-контракта. То есть на прямую к нашему СК мы не обращаемся, мы обращаемся к прокси СК, который будет реализовывать все функции при помощи delegateCall и как мы знаем хранить в себе все данные.

User ---- tx ---> Proxy ----------> Implementation_v0
                     |
                      ------------> Implementation_v1
                     |
                      ------------> Implementation_v2
Пример из openzeppelin

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

Дальше будет код на yul, низкоуровневом языке solidity, и если вы не знакомы с ним и EVM ethereum, будет не совсем понятно.

Контракт proxy:

 // SPDX-License-Identifier: MIT
abstract contract Proxy {        
    function _delegate(address implementation) internal virtual {        
        assembly {                       
            calldatacopy(0, 0, calldatasize())                    
            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result                
            case 0 {               
               revert(0, returndatasize())            
            }            
            default {                
                return(0, returndatasize())            
            }        
        }      
    }    
    function _implementation() internal view virtual returns (address);
    
    function _fallback() internal virtual {        
        _beforeFallback();        
        _delegate(_implementation());    
    }
    fallback() external payable virtual {        
        _fallback();    
    }
    receive() external payable virtual {       
        _fallback();    
    }
    function _beforeFallback() internal virtual {}
}

Для того, чтоб вызывать функции нашего implementation(СК), мы будем использовать функцию fallback, которая вызывается всегда, когда в СК поступает вызов функции, которой не существует.

Функция _fallback() будет вызывать функцию _delegate, в которой происходит вся магия.

Функция _beforeFallback, может в себе реализовать что то перед вызовом fallback, например какую то проверку.

Функция _implementation, помечена как virtual, потому что ее мы будем переопределять в другом месте, чтоб спрятать слот хранения нашего адреса (реализация ERC1967). Она будет возвращать адрес нашего смарт-контракта, который мы реализуем в Proxy. О стандарте ERC1967 в следующий раз, сегодня просто про принцип работы.

Теперь сама функция _delegate:

При помощи низкоуровневого языка, мы копируем в память calldatasize (размер данных, например селектор функции, аргументы), при помощи delegatecall вызов перенаправляется в наш СК implementation. Указываем газ, чтоб транзакция прошла, адрес implementation, данные, ну и возвращаем returndatacopy (данные после вызова, скопированные после выполнения функции delegatecall). Дальше проверяем если наш result равен нулю то revert, иначе возвращаем результат выполнения.

Таким образом все вызовы приходящие на Proxy будут делегированы ему, и вызваны в его контексте. Но важно понимать, что все переменные storage в Proxy СК должны быть в том же порядке, как и в implementation.

Важно! Это пример реализации и его лучше не использовать в своих проектах, но при этом принцип работы мы должны знать как хорошие программисты. Лучше импортировать из библиотеки openzeppelin проверенную версию и не париться.

Так же хочу добавить одну вещь, которую вы должны знать. Если вы используете в своем СК конструктор, то для upgreadeble смарт-контрактов его использовать не получиться, потому что конструктор вызывается 1 раз при деплое и к его байт коду мы не сможем обратиться через proxy. Грубо говоря в байт коде выполняемого смарт-контракта нету байт кода конструктора и поэтому мы не сможем к нему обратиться. Следовательно Proxy СК не сможет установить, например, овнера через конструктор, так как не увидит конструктор.

Решение очень простое. Мы просто будем использовать псевдо конструктор, где реализуем все что нам нужно.

 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; 
  
  contract MyContract is Initializable { 
     function initialize( address arg1, uint256 arg2, bytes memory arg3 ) public payable initializer { 
         // "constructor" code... 
     } 
 }

Вот примерно так выглядит наш новый конструктор.

Модификатор initializer работает так, чтоб вызов данной функции был единожды, как у конструктора.

И таким образом конструктор будет виден Proxy

Ухх, если вы дожили до сюда, то мой поклон. Сегодня разобрали 4 паттерна, которые можно использовать в своих СК, для реализации каких либо нужд. Дальше больше. Разумеется это не все паттерны, их еще очень много. Но самые интересные и полезные я показал.

tg: мой телеграмчик)

GitHub: Этот проект на гит хабе