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

Интерфейс IERC721Receiver

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

Интерфейс IERC165

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

Абстрактный contract ERC165 is IERC165

Если контракт хочет поддерживать ERC165, то он должен наследовать и переписать функцию supportsInterface() из данного контракта.

Интерфейс IERC721 is IERC165

Стандартная реализация. Ее мы подробно рассматривали ранее в урока про создание NFT.

Интерфейс IERC721Metadata is IERC721

Добавление метаданных к NFT: имени, символа и url.

Контракт ERC721 is Context, ERC165, IERC721, IERC721Metadata

Создание токена NFT, включая метаданные.

Абстрактный contract ERC721URIStorage is ERC721

Управление url нашего NFT.

#soulbound #nft #token
👍1
Создание soulbound NFT. Часть 3

Далее идет наш контракт UniversityDegree is ERC721URIStorage.

Создаем переменную owner и два mapping: personToDegree, со сиском тех, кто получил свой токен, issuedDegrees, для проверки был ли выдан токен данному пользователю.

Подключаем библиотеку Counters для отслеживания новой переменной _tokenIds.

Незабываем про модификатор onlyOwner. А также в конструкторе передаем имя токена и его символ в контракт ERC721.

Функция checkDegreeOfPerson() возвращает токен пользователя, а issueDegree() добавляет пользователя к исполненным выдачам.

Функция claimDegree() позволяет пользователю забирать свои NFT. Она проверяет был ли уже ранее выдан токен, увеличивает id нового токена, минтит NFT и прикрепляет к нему ссылку, а также прикрепляет данный NFT к пользователю и добавляет его в issuedDegrees.

Достаточно простой контракт. Однако, где же здесь souldbound? На вид представлено достойное создание обычного NFT. А вся соль в следующем.

В стандарте ERC721 есть функция safeTransferFrom(), которая и отвечает за возможность передачи NFT другим пользователям. Удалив ее, мы фактически сделаем NFT привязанным к конкретному адресу.

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

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

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

#soulbound #nft #token
👍1
Создание soulbound NFT. Часть 4

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

Pinata доступна по этой ссылке.

В бесплатной версии можно загрузить до 100 файлов и не более на 1 Gb.

После регистрации вы попадаете в личный кабинет. Там нужно нажать на кнопку "Upload+" и выбрать нужный файл. Через пару секунд он отобразиться в кабинете.

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

Все легко и просто. Не зря он стал одним из самых популярных сервисов для хранения NFT.

#soulbound #nft #token #pinata
👍1
Сдвиги / смещение

Прикольную штуку сейчас в чате по Solidity встретил.

Задача

В общем, есть 4 переменных uint64 и нужно передать в функцию как одну uint256, а потом сдвигами их доставать.

Решение

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

И вот, как эту задачу можно решить:

Сначала объединяете все переменные в одну uint256, а потом достаете значение так:

uint64 a1 = uint64(a);
uint64 a2 = uint64(a >> 64);
uint64 a3 = uint64(a >> 128);
uint64 a4 = uint64(a >> 192);

Круто, да?

#uint #memory #сдвиг #смещение
👍1
Тема недели - Uniswap

Слегка запоздалый пост для понедельника, но все же.

Как и планировали, на этой недели мы плотно посидим с разбором биржи Uniswap. А точнее взаимодействия с ней через наши смарт контракты.

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

На данный момент больше уроков в сети есть для v2 версии биржи, но актуальной является v3. Поэтому я часть примеров буду брать из документации, часть из некоторых видео на Youtube.

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

Приятной недели и легкого обучения!
👍1
О различиях V2 и V3

Я нашел два видео, которые могут быть вам полезны, чтобы лучше понять работу Uniswap.

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

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

Итак, первое видео про v2 и v3.

Второе видео про работу пулов ликвидности.

#uniswap #pool #liqudity
Некоторые сложности с Uniswap

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

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

Во-первых, по v3 мало уроков даже на английском языке. Есть по использованию их сайта, есть по sdk, но именно код смарт контракта рассматриваетяся также из документации.

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

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

В-третьих, для некоторых вещей вообще используют только v2.

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

Поэтому...

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

Если же ничего не получится, то возьмем пару уроков от Ильи с Youtube, а также разберем пару примеров аудита контрактов, а к Uniswap вернемся уже после.

Просто держу вас в курсе. Не теряйте!
👍1
Пишем мини краудфандинг

Я вчера просидел около 8 часов в общей сложности, пытаясь разобраться с Uniswap. Но, чем больше вникал, тем больше вопросов появлялось. Особенно в экономической сфере. Поэтому я решил чуть перенести уроки на попозже.

Если кто-то вдруг встретит хорошие уроки по Uniswap, пожалуйста, дайте знать.

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

Видео урок на Youtube.

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

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

Приятного дня и легкого обучения.
👍1
Разбор кода краудфандинга. Часть 1

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

И так, основной файл содержит два контракта: родительский и порождаемый. Пройдемся по первому.

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

Далее кладем все в mapping для удобного хранения и доступа, а также создаем несколько переменных: на максимальную продолжительность кампании, id текущей кампании и адрес владельца.

Функция start() принимает в себя два аргумента: когда кампания должна закончиться (ее продолжительность) и сумма, которую планируют собрать. Также внутри находятся две проверки, чтобы пользователи не создавали кампании без цели и сроков.

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

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

Другая функция onClaimed() устанавливает статус кампании, как исполненной, в случае ее успеха. 

Далее идет дочерний \ порождаемый контракт.

#croudfunding
👍1
Разбор кода краудфандинга. Часть 2

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

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

Далее создаем mapping, куда записываем информацию о взносах.

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

Функция pledge() позволяет сторонним пользователям делать взносы. Тут нужны проверки на продолжительность кампании и размер взноса, чтобы он не был нулевым. В конце просто добавляем записи в mapping.

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

Функция claim() может вызывать только создатель кампании в случае ее успеха. Тут устанавливается статус, выполняется перевод на счет организатора и вызывается функция onClaimed() в родительском контракте.

Также есть fullRefund(), которую, по сути, также  должен вызывать только организатор, или она должна выполняться автоматически по прошествии времени, в случае неудачи сбора средств. Она возвращает деньги всем пользователям, кто ранее совершал взнос.

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

#croudfunding
👍1
Разбор тестов краудфандинга. Часть 3 

Обратите внимание, что тесты в этом случае писались с использованием typenoscript, и расширение файла - .ts.

В этих тестах меня привлекло следующее решение лектора. Он создал отдельный файл "setup.ts", куда импортировал:

import {loadFixture, time} from "@nomicfoundation/hardhat-network-helpers";
import {ethers} from "hardhat";
import {expect} from "chai";
import "
#nomicfoundation/hardhat-chai-matchers";
export {loadFixture, ethers, expect, time};

а уже в рабочем файле тестов одной строкой получил все необходимое оттуда:

import { loadFixture, ethers, expect, time } from "./setup";

Также импортируются типы для контрактов:

import type { LowkickStarter } from "../typechain-types";
import { Campaign__factory } from "../typechain-types";

Если не понимаете, о чем идет речь, то поищите урок на канале про typechains.

В начале теста нам необходимо делать деплой контракте с помощью функции dep(). Здесь также все как обычно просто.

И, наконец, сам тест.

Прежде всего получаем необходимые данные для работы через:

const { lowkick, owner, pledger } = await loadFixture(dep);

Далее нам нужно создать дочерний контракт, вызвав функцию strat() в нем. Тут интерес представляет строка, где устанавливается продолжительность кампании:

const endsAt = Math.floor(Date.now() / 1000) + 30;

Дело в том, что в javanoscript время исчисляется в миллисекундах, а в контрактах - в секундах. Поэтому нам нужно привести все к общему знаменателю таким способом.

Затем мы получаем адрес нашего контракта, вытаскивая его по id из mapping родительского контракта с ключом targetContract.

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

const campaignAsOwner = Campaign__factory.connect(campaignAddr,owner);
const campaignAsPledger = Campaign__factory.connect(campaignAddr,pledger);

Проводим первый тест, сравнивая продолжительность кампании:

expect(await campaignAsOwner.endsAt()).to.eq(endsAt);

Второй тест, что организатор не сможем забрать деньги до окончания кампании:

await expect(campaignAsOwner.claim()).to.be.reverted;

и сама кампания не будет исполненной:

expect((await lowkick.campaigns(1)).claimed).to.be.false;

В конце увеличиваем время:

await time.increase(40);

И проверяем, что claim() работаем и деньги переводятся на счет организатора при успешном завершении:

await expect(() => campaignAsOwner.claim()).to.changeEtherBalances([campaignAsOwner, owner], [-1500, 1500]);

В тестах меня всегда немного смущала логика: кому, когда и к чему нужно подключаться. Но когда вот так рассматриваешь контракты и тесты, то все становится понятнее.

Я каждому рекомендую немного посидеть с тестами и попрактиковаться.

#croudfunding #test
👍1
Еще урок от Ильи

Предлагаю посмотреть еще один, последний на данный момент, урок, который расскажет нам чуть больше о тестах в hardhat и проблеме BigNumber.

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

Разбора этого видео не будет, так как оно носит скорее информативный характер.

Итак, новый видео урок.
👍2
Hardhat network helpers

Мы в недавних тестах контрактов использовали такие штуки, как loadFixture и time, которые являются хелперами hardhat и сильно упрощают нашу работу.

Насколько я знаю, сейчас пакет этих хелперов идет вместе с toolbox при установке hardhat, но если что, их можно установить отдельно командой в консоли:

npm install --save-dev @nomicfoundation/hardhat-network-helpers

и затем импортировать в нужный файл через

import { % } from "@nomicfoundation/hardhat-network-helpers";


Давайте рассмотрим другие хелперы.


mine([blocks], [options])
- майнит определенное количество блоков с заданным интервалом, например mine(1000, { interval: 15 }).

mineUpTo(blockNumber) - майнит новые блоки до указанного номера.

setBalance(address, balance) - устанавливает баланс в wei на нужном адресе, например setBalance(address, 100n ** 18n).

setCode(address, code) - изменяет байткод контракта, хранящегося на адресе аккаунта.

setNonce(address, nonce) - изменяет nonce аккаунта, переписывая его. Ранее я не сталкивался с nonce, поэтому сложно интерпретировать его. В переводе nonce - число, время.

setStorageAt(address, index, value) - записывает в слот памяти контракта значение, например, setStorageAt(address, storageSlot, newValue).

getStorageAt(address, index, [block]) - извлекает данные, расположенные по заданному адресу, индексу и номеру блока. Block можно указать номером, а можно словом "latest", "earliest" или "pending" (последний, первый, текущий).

impersonateAccount(address) - позволяет hardhat network подписывать последующие транзакции по данному адресу.

stopImpersonatingAccount(address) - останавливает impersonateAccount(address).

latest() - возвращает timestamp последнего блока.

latestBlock() - возвращает номер последнего блока.

increase(amountInSeconds) - майнит блок через указанное время от последнего блока.

increaseTo(timestamp) - майнит блок с указанным timestamp.

setNextBlockTimestamp(timestamp) - устанавливает timestamp последующего блока, но не майнит его.

takeSnapshot() - снимает скрин текущего состояния блокчейна на текущий блок.

loadFixture() - устанавливает желаемое состояние сети блокчейна. Пример использования можно найти в уроках выше.

dropTransaction(txHash) - удаляет требуемую транзакцию из mempool, если таковая существует. Mempool - по сути, некое место ожидания для транзакций на их выполнение.

setNextBlockBaseFeePerGas(baseFeePerGas) - устанавливает базовую стоимсоть следующего блока.

setPrevRandao(prevRandao) - устанавливает значение PREVRANDAO для следующего блока.

Note: "После перехода на PoS, чтобы сохранить работоспособность предыдущего кода Solidity, разработчики Ethereum решили изменить базовый код операции block.difficulty 0x44, чтобы он возвращал обновленное PoS поле, которое и называется prevrandao."

#hardhat #helpers
👍1
Безопасность и аудит смарт контрактов

Вчера наш подписчик ohDiscord1a скинул в чат канала интересный репост о взломе смарт контракта, история которого развернулась на просторах Твиттера.

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

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

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

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

Это не сделает ваш код на 100% защищенным от взлома, и тем не менее, он станет в разы лучше!

Приятного дня и легкого обучения!
👍1
Подготовка смарт контракта к аудиту

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

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

2. Документируйте все, что является публичным API контракта.

3. Большой плюс, если вы будете использовать NatSpec формат (о нем подробнее в следующих постах).

4. Перед аудитом пропишите тесты каждой функции, require, event и т.д. Хороший инструмент для таких целей npm пакет solidity-coverage (входит в hardhat toolbox по умолчанию).

5. Избавьтесь от всех ошибок в контракте для его успешной компиляции.

6. Не старайтесь переоптимизировать свой код. Порой критические ошибки появляются именно там. При оптимизации пишите комментарий о том, для чего это делается в этом конкретном случае.

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

8. Удаляйте старые комментарии, неиспользуемый код и функции.

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

10. Не делайте изменения в коде (комите на GitHub) во время аудита кода. Это сильно усложняет работу.

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

12. В комментариях аудитору опишите свои планы, как вы планируете использовать свои контракт в будущем: обновлять, дополнять, наследовать и т.д.

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

14. Не надейтесь, что аудит кода на 100% убережет вас от взлома. Это лишь еще одна ступень безопасности!

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

#аудит #безопасность #взлом #hack
👍1
Что такое NatSpec в смарт контракте?

NatSpec (Ethereum Natural Language Specification Format) - это специальная форма комментариев для документирования вашего смарт контракта.

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

Простой пример:

/**
*
@noscript Counters
*
@author Matt Condon (@shrugs)
* @dev Provides counters that can only be incremented, decremented or reset. This can be used e.g. to track the number
* of elements in a mapping, issuing ERC721 ids, or counting request ids.
*/

Вообще, существуют следующие теги:

@noscript - описывает контракт, библиотеку или интерфейс;
@author - автор контракта, библиотеки или интерфейса;
@notice - заметка о функции, переменной, контракта и т.д.;
@dev - описывает для разработчиков какие-либо детали кода;
@param - документирует параметры функции или event;
@return - говорит, что возвращается в функции;
@inheritdoc - копирует теги из базового контракта (обязательно нужно указывать сам контракт);
@custom - пользовательский комментарий;

Больше про теги и комментирование можно прочитать в официальной документации Solidity тут.

#аудит #natspec
👍1
Безопасность. Внешние вызовы. Часть 1

Вызовы (далее external calls) в другие контракты, код которых вы не знаете, могут привести к значительным проблемам безопасности вашего контракта.

Постарайтесь как можно реже использовать external calls в своем контракте. Однако, если все же это необходимо, то придерживайтесь следующих правил.

Помечайте контракты, которые вы не знаете

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

Например:

// плохо

Bank.withdraw(100); // Unclear whether trusted or untrusted

function makeWithdrawal (uint amount) {
    // Isn't clear that this function is potentially unsafe
    Bank.withdraw(amount);
}

// хорошо

UntrustedBank.withdraw(100); // untrusted external call
TrustedBank.withdraw(100); // external but trusted bank contract maintained by XYZ Corp

function makeUntrustedWithdrawal (uint amount) {
    UntrustedBank.withdraw(amount);
}

#безопасность #externalcall #external
👍1
Безопасность. Внешние вызовы. Часть 2

Избегайте изменений переменных состояния при external calls

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

#безопасность #externalcall #external
👍1
Безопасность. Внешние вызовы. Часть 3

Не используйте transfer() или send()

Оба метода используют фиксированное количество газа (2300), как изначально предполагалось для защиты от reentrancy. Однако с одним из последних обновлений (EIP 1884) количество газа в этих функциях может быть увеличено.

Рекомендуется использовать call().

Пример:

// плохо

contract Vulnerable {
    function withdraw(uint256 amount) external {
        // This forwards 2300 gas, which may not be enough if the recipient
        // is a contract and gas costs change.
        msg.sender.transfer(amount);
    }
}

// хорошо

contract Fixed {
    function withdraw(uint256 amount) external {
        // This forwards all available gas. Be sure to check the return value!
        (bool success, ) = msg.sender.call.value(amount)("");
        require(success, "Transfer failed.");
    }
}

P.S. call() также не защищает от reentrancy, просто в документации советуют использовать именно его.


#безопасность #externalcall #external
👍1
Безопасность. Внешние вызовы. Часть 4

Учитывайте ошибки внешних вызовов

// плохо

someAddress.send(55);

someAddress.call.value(55)("");
// опасно, так как использует весь газ и не проверяет на ошибки

someAddress.call.value(100)(bytes4(sha3("deposit()")));
// если deposit() получит ошибку, то call() вернет false, но транзакция все равно отправит деньги

// хорошо

(bool success, ) = someAddress.call.value(55)("");
if(!success) {
    // handle failure code
}

Всегда проверяйте успешность низкоуровневых вызовов!

#безопасность #externalcall #external
👍1
Безопасность. Внешние вызовы. Часть 5

Используйте "запросы" вместо "автоматики"

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

Этот подход также помогает оптимизировать расход газа в контракте.

Не используйте delegatecall

delegatecall вообще опасная штука. Разбор кода со взломом контракта через этот метод есть на канале. Если вы на 100% не уверены, что и как будет выполнять функция с ним, то старайтесь не использовать его совсем.

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

#безопасность #externalcall #external
👍1