Solidity. Смарт контракты и аудит – Telegram
Solidity. Смарт контракты и аудит
2.62K subscribers
246 photos
7 videos
18 files
547 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
Описание контракта Governance из урока. Часть 2

Для начала нам потребуется функция для создания уникального id предложения. Поэтому создаем generateProposalId(), куда передаем информацию о самом предложении (адрес, сумму, функцию для вызова, данные, и захешированное описание), и через знакомый нам keccak256 кодируем все и возвращаем.

Также создаем функцию для определения состояния (enum) предложения. Для этого берем из storage (так как предложение уже находится там после своего создания) и проверяем значения, в соответствии с которыми выставляем статус состояния.

Затем можно написать функции создания, исполнения и голосования.

В функции создания предложения мы принимаем необходимую информацию, создаем уникальный id через generateProposalId() и сохраняем в mapping структуру. При этом не забываем выполнить проверку на наличие токенов у пользователя, для создания предложения. И в конце возвращаем id.

В функции голосования мы делаем проверки на наличие токенов у пользователя, статуса предложения (ведь он не должен быть уже состоявшемся), а также голосовал ли ранее этот пользователь по данному предложению. В зависимости от его выбора, добавляем голос в значения "за", "против" или "воздержался". Ну, и выставляем статус, что данный пользователь успешно проголосовал.

В функции исполнения голосования мы принимаем все те же данные, что и в предыдущих функциях. Генерируем id и проверяем его статусы и наличие в базе.

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

#dao #governance
Деплой и тесты контрактов из урока. Часть 3

В конце хочу сказать пару слов о деплое и тестах в данном уроке: я ничего не понял.

В этом уроке лектор использовал typenoscript и typechain, с которыми я ранее не сталкивался в этой реализации. Поэтому, если вы также ничего не поняли, есть два варианта:

1. Вы можете сами поискать информацию об этом в сети, сделать пост или поскидывать материал в чат канала;

2. Немного подождать. Про typechain есть отдельный урок у лектора, который мы будем проходить послезавтра. А по деплою контрактов я планирую выделить день-два на следующей неделе. Там я постараюсь найти информацию о плагинах, библиотеках, функциях и т.д. по этой теме, и написать разборы.

Вообще не переживайте, если не понимаете объяснения лектора или информацию из моих постов. Я сам возвращаюсь к прошлым урокам довольно часто, так как многое забывается через день-два.

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

Мы учим язык всего 1,5 месяца. И если вы занимаетесь вместе со мной, то уже знаете столько, сколько другой ученик получает за пол-года или даже больше! И этим точно можно гордиться!

#dao #governance
Урок 22 - Паттерн Proxy/Upgradeable: Transparent, UUPS

Сегодня у нас очень интересный урок, особенно для новичков в Solidity.

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

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

Видео урок о proxy.

Что нужно взять из этого урока?

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

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

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

Приятного дня и легкого обучения!

#урок #proxy #upgradeable #transparent #uups
👍2
Паттерн Proxy

Основная проблема смарт-контрактов заключается в том, что после деплоя в блокчейн их нельзя изменить или удалить.

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

С этими проблемами и призван бороться прокси паттерн. Так как он работает?

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

По факту прокси будет выполнять функционал нашего смарт-контракта, но в своем контексте за счет такой штуки, как delegatecall.

Этот подход позволяет разделить логику работы смарт-контракта и данные, с которыми мы работаем. Получается, что логика лежит в нашем СК, а данные - в прокси.

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

#proxy
👍1🔥1
Паттерн Transparent Proxy

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

Однако остается вопрос, а что делать, если у нас две одинаковые функции, два одинаковых селектора, и в прокси и в контракте исполнения?

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

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

#proxy #transparent
👍1
Паттерн UUPS Proxy

UUPS или Universal Upgradeable Proxy Standard (Универсальный Обновляемых Прокси Стандарт) - более новый и легковесный прокси контракт.

В случае UUPS все админские функции находятся в контракте исполнения, а не в самом прокси. По сути, тут нет ничего, за исключением логики перенаправления в другой контракт.

#proxy #uups
👍2
Паттерн Diamond Proxy

Есть еще один вид прокси контрактов под названием Diamond. В этом уроке мы его не рассматриваем, но упомянуть стоит.

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

В этом случае исполняемый контракт разбивается не несколько более мелких контрактов, которые и управляются через прокси.

#proxy #diamond
Простая идея реализации прокси контракта

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

Я просто повторю основные моменты тут.

Есть контракт Proxy, в котором всего три функции: setImplementation(), _delegate() и fallback().

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

В _delegate() как раз прописывается логика отправки запроса и получения ответа в исполняемый контракт при помощи delegatecall.

Тут интересно то, что написана она с помощью assembly. Мы принимает селектор функции из исполняемого контракта (берем его из памяти), затем передаем через delegatecall в другой контракт и принимаем оттуда ответ. Если приходит "0", то показываем ошибку, так как данных нет, а значит в другом контракте что-то пошло не так. Если данные получены, то показываем их.

Также интерес представляет функция fallback(). Как мы помним, она вызывается в том случае, когда в контракте нет функции с таким именем, которую пытаются тут вызвать. Следовательно, в этом случае вызывается fallback, которая в свою очередь вызывает _delegate.

И вот по этой цепочке и происходит работа прокси. При этом все данные остаются тут, а не в исполняемом контракте.

Это самый простой способ работы с прокси, поэтому еще раз повторяю, что его не следует использовать в реальных проектах!

#proxy #upgradeable #transparent #uups
👍1
Наследование из openzeppelin

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

Сделать это можно двумя способами. И так как тема у нас про обновляемые контракты, то сразу покажу установку для них.

1. Вам необходимо прописать следующие строки в консоли:

npm install @openzeppelin/contracts
npm install --save-dev
@openzeppelin/hardhat-upgrades
npm install --save-dev
@nomiclabs/hardhat-ethers ethers # peer dependencies

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

2. Можно также зайти в файл package.json в папке своего проекта, и в блоке dependencies прописать:

"@openzeppelin/contracts": "^4.7.2",
"
@openzeppelin/contracts-upgradeable": "^4.7.2"

Затем зайти в консоль и выполнить команду:

npm install

После чего у вас установятся все необходимые пакеты.

При этом после добавления openzeppelin/contracts-upgradeable вам следует открыть файл hardhat.config.json в своем проекте и добавить строки в начале:

import "@openzeppelin/hardhat-upgrades";
(если вы используете typenoscript)

или

require('@openzeppelin/hardhat-upgrades');
(если используете javanoscript)

После этого уже в файле своего контракта вы сможете импортировать контракты openzeppelin и наследовать от них. Для этого нужно будет добавлять следующие строки (как в примере урока):

import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "
@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";

и т.д.

#proxy #upgradeable #npm #openzeppelin
Upgradeable ERC721. Часть 1

Отдельный урок про ERC721 у нас будет на следующей неделе, так как потребуется пара дней, чтобы научиться работать с ним. А сейчас мы, на его примере, рассмотрим, как работают обновляемые контракты.

Для начала нам нужно импортировать все необходимые обновляемые контракты с openzeppelin, а также прокси контракт Initializable.

Коммит по уроку можно посмотреть тут.

Так как конструкторы, как я понял из урока, в обновляемых контракта могут работать некорректно, было принято решение заменить его новой функцией initialize() с модификатором initializer. По правилам она вызывается только один раз при разворачивании контракта, поэтому ее можно как бы назвать неким конструктором обновляемых контрактов.

И уже в этой функции мы можем другие функции, с помощью которых можно установить владельца контракта или передать данные о токене, например, в ERC721.

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

#proxy #upgradeable #transparent
👍1
Upgradeable ERC721. Часть 2

По умолчанию все обновляемые контракты используют transparent proxy. Для того, чтобы переключиться на UUPS proxy нам нужно сделать следующие действия.

1. Добавить в импортируемые файлы с openzeppelin строку

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

2. Добавить наследование от UUPS в наш контракт;

3. В функции initialize() добавить функцию __UUPSUpgradeable_init(), чтобы при разворачивании система поняла, что мы используем именно UUPS;

4. Также вспоминаем, что в случае UUPS proxy все права администратора записываются в обновляемом контракте, т.е. том, который пишем мы. Поэтому нужно добавить функцию администрирования.

function _authorizeUpgrade(address newImplementation) internal onlyOwner override {}

Этих действий будет достаточно, чтобы развернуть UUPS прокси к нашему контракту.

#proxy #upgradeable #uups
Деплой Upgradeable ERC721. Часть 3

Отдельно деплой контрактов мы будем разбирать на следующей неделе, а пока пройдемся по коду из урока.

В начале файла лектор подключает три импорта:

import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers, upgrades } from "hardhat";

С "chai" и "ethers" мы уже знакомы, поэтому остановлюсь на двух других.

"upgrades" - это плагин, который стал доступен, когда мы установили пакет hardhat-upgrades. Он помогает разворачивать и тестировать прокси контракты.

"loadFixture" - это также новый плагин, который был добавлен в toolbox. Лектор, говорит, что это новая фича в hardhat, но на момент, когда мы учились устанавливать среду разработки, он уже был доступен и я рассказывал, как его установить.

В данном тесте у нас нет обычной функции beforeEach(), которая вызывалась перед каждым тестом. В этом случае мы пишем отдельную функцию dep() (или деплой).

Также используем ethers.getSigners(), чтобы получить адрес пользователя, который разворачивает контракт, а также getContractFactory(), чтобы указать, с каким контрактом мы работаем.

И вот дальше мы используем плагин "upgrades", для деплоя прокси контракта, где в аргументах передаем разворачиваемый контракт, аргументы для функции initialize() в нашем контракте, если необходимо, и набор опций для прокси:

const token = await upgrades.deployProxy(NFTFactory, [], {
  initializer: 'initialize',
  kind: 'uups',
});

Если мы работаем с UUPS контрактами, то добавляем сюда kind: "uups". Если же с transparent proxy, то оставляем только initializer.

Из этой функции деплоя нам нужно вернуть контракт и адрес деплоера.

Далее в тестах, с помощью плагина loadFixture(), мы обращаемся к функции нашего деплоя, откуда получаем token (наш прокси) и deployer.

const { token, deployer } = await loadFixture(dep);

После этого можно писать тесты, как мы уже делали раньше.

Далее посмотрим, как работать с обновленным контрактом в тестах.

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

const token2 = await upgrades.upgradeProxy(token.address, NFTFactoryv2);

После этого мы можем писать тесты уже для обновленного контакта. 

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

#proxy #upgradeable #transparent #uups #deploy
Урок 23 - Typechain и hardhat toolbox

Заканчиваем нашу ударную неделю в Solidity с изучением урока про typechain.

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

Как оказалось, использование loadFixture более современный метод деплоя, и уже можно не писать beforeEach(). Более того лектор немного ругается про плагин hardhat deploy, который они часто использовал ранее, обосновывая, что там есть ошибки, которые "ломают" npx.

Но о деплое мы поговорим еще на следующей неделе, а сегодня будут посты про typechain.

Видео урок про typechain и toolbox.

Для меня это тоже новая тема, поэтому буду писать, как я сам понимаю реализацию. Если среди вас есть те, кто уже работал с typenoscript, то буду рад комментариям или поправкам.

Приятного дня и легкого обучения!

#урок #typechain #toolbox
Немного о Typechain

Для начала нужно разобраться, а что такое typechain и зачем он нужен в hardhat?

Typechain - это решение, которое уже входит в hardhat toolbox и генерирует для наших смарт-контрактов специальные типы для typenoscript. Они помогают нам работать с объектами СК.

Попробую объяснить чуть понятнее.

До этого мы писали все тесты в файле с расширением js, т.е. javanoscript.

JavaScript потрясающий язык программирования, но в нем есть некоторые проблемы с типами данных. Напомню, что типы данных это string, uint, bool и т.д. Только в js не uint, а number.

Так вот, в работе с этими данными в некоторых случаях нужно было конкретно указывать js, что мы работаем, например, с числами, иначе могло получиться так, что "1+1" было бы равно "11", а не "2".

Для решения этой и других проблем языка был создан typenoscript. Я плохо с ним знаком, но могу сказать, что уже на старте работы там требуется конкретизировать тип данных в переменных.

И как я понял из урока, typechain использует typenoscript для работы с объектами СК, а именно дает подсказки по функциям и типам данных в них.

Более того, немного покопавшись в других примерах разработки СК, я понял, что typechain более "крутой" и современный подход. И теперь нам нужно будет стараться учиться писать тесты, используя именно его.  

#урок #typechain
👍1
Начало работы с Typechain

В самом начале, когда мы только установили hardhat в папку нашего проекта и вызвали команду npx hardhat, нам предлагали сделать выбор:

Create a JavaScript project
Create a TypeScript project

Так вот сейчас нам нужно выбрать второй пункт.

Обратите внимание, что файл hardhat.config теперь имеет расширение ts, а не js, как было раньше.

Проверьте, чтобы в этом файле были подключены следующие импорты:

import { HardhatUserConfig } from "hardhat/config";
import "
@nomicfoundation/hardhat-toolbox";

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

Далее нам нужно написать наш смарт контракт и скомпилировать его с помощью команды npx hardhat compile.

В папке проекта должна появиться новая папка typechain-types, где лежит файл с именем нашего контракта. И этот файл нам нужно подключить в файл теста, по примеру:

import { Sample } from "../typechain-types";

Также в функции деплоя контракта мы должны определить наш тип:

const SampleFactory = await ethers.getContractFactory("Sample");
const sample: Sample = await SampleFactory.deploy();

Именно то, что мы определили тип данных нашего контракта после знака ":", поможет нам работать с этим объектом далее в тестах.

После этих действий, когда в тестах нам потребуется вызвать какую-либо функцию объекта, typechain будет давать подсказки, включая необходимые аргументы функции.

#урок #typechain
Подключение к уже развернутому контракту с Typechain

Для подключение к развернутому контракту существует такая штука, как factories в папке typechain-types.

Тут можно найти abi контракта, его байткод и т.д.

И если в своих тестах вы хотите подключиться к контракту нет из под адреса деплоера, а из под другого адреса, то можно использовать функцию connect() из factories.

Для этого нужно в импорт typechain в начале файла добавить factories:

import { Sample, Sample__factory } from "../typechain-types";

После этого, подключиться к другому адресу можно через:

const sampleAsUser = Sample__factory.connect(sample.address, user);

где первый параметр - это адрес контракта, а второй - адрес, к которому подключаемся.

#урок #typechain #connect
Урок 24 - ERC721 и NFT

Привет всем!

Надеюсь выходные вы провели не только праздно, но и с небольшой пользой, а именно до разбирали уроки прошлой недели.

Да, они были несколько сложные, но и мы уже не новички в Solidity.

На этой неделе я планирую закончить серию уроков продвинутого изучения Solidity, разобрав такие штуки как ERC721, NTF, деплой, МетаМаск, Honeypot (который был выпущен лектором вчера) и пара рекомендаций, включая вопросы к собеседованию часть 2.

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

А пока, вот видео урок про ERC721 и NFT.

Как я сам понял, в таких стандартах как ERC20, ERC721, ERC3475, важно понимать набор основных функций и переменных, чтобы уже на основе них создавать свои контракты.

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

Всем приятного дня и легкого обучения.

#урок #erc721 #nft
Описание контрактов ERC721 и NFT. Часть 1

Как обычно, для начала разберемся, что такое ERC721.

Стандарт ERC721, по своей сути, всего лишь регламентирует то, как наши NFT должны храниться в блокчейне, как их можно передавать друг другу, вводить в оборот и уничтожать. А уже в другом нашем контракте мы создаем (минтим) NFT на основе ERC721, добавляем имя, символ и ссылку, где он хранится.

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

В контракте ERC721.sol лектор подключает несколько импортов:

import "./ERC165.sol";
import "./IERC721.sol";
import "./IERC721Metadata.sol";
import "./Strings.sol";
import "./IERC721Receiver.sol";


IERC721.sol
- простой интерфейс со списком функций нашего контракта.

IERC721Metadata.sol - интерфейс, который добавляет функции имени, символа и ссылки на наш NFT.

Strings.sol - библиотека из openzeppelin, которая помогает переводить uint в string.

ERC165.sol - контракт, от которого мы наследуем в своем контракте функцию, которая как бы говорит, что наш контракт может принимать и работать с NFT. Другими словами, когда чужой контракт будет спрашивать наш, работает ли он с интерфейсом ERC721, то наш контракт ответит ему положительно.

IERC721Receiver.sol - реализует функцию onERC721Received(), которая проверяет, кто принимает наш NFT: контракт или адрес, и помогает получать ошибку. О нем поговорим подробнее ниже.

В уроке, получается, наш контракт ERC721 наследует от трех других:

contract ERC721 is ERC165, IERC721, IERC721Metadata

Далее рассмотрим переменные, конструктор и базовые функции контракта.

#erc721 #nft
Описание контрактов ERC721 и NFT. Часть 2

В начале контракта устанавливаются две переменные - имя и символ.

Далее, мы прописываем 4 mapping:

1. Первый, который будет рассказывать, сколько NFT на определенном адресе;
2. Второй - кто владеет данным NFT;
3. Третий - разрешение такому-то адресу управлять NFT с таким-то id, например, переводить его на другой адрес.
4. Четвертый показывает, что этот конкретный адрес может управлять всеми токенами на другом конкретном адресе.

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

Затем мы пишем конструктор, в котором передаем имя и символ нашего токена и приравниваем их к переменным, созданным выше.

Думаю, тут же стоит обозначить несколько простых функций из контракта.

name() - возвращает имя токена;

symbol() - возвращает символ токена;

balanceOf() - принимает адрес и показывает баланс на нем, беря информацию из mapping;

ownerOf() - принимает id токена и возвращает адрес владельца, беря его из второго mapping;

getApproved() - принимает id токена и возвращает адрес, беря его из третьего mapping;

isApprovedForAll() - принимает id токена и возвращает булево значение, получая его из четвертого mapping;

burn() - принимает id токена, проверяет, может ли данный пользователь распоряжаться им, и передает в служебную функцию _burn(), которая, в свою очередь, получает владельца токена, уменьшает его баланс и удаляет информацию о токене из второго и третьего mapping;

_beforeTokenTransfer() и _afterTokenTransfer() - служебные пустые функции, которые используются уже в создании токена в другом контракте;

В следующей части поговорим о более сложных функциях контракта.

#erc721 #nft