ru
Feedback
Solidity. Смарт контракты и аудит

Solidity. Смарт контракты и аудит

Открыть в Telegram

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

Больше
2 598
Подписчики
+124 часа
+17 дней
+330 день
Архив постов
Разбор контракта ERC1155. Часть 2 Теперь самое интересное! Есть две функции пересылки токенов _safeTransferFrom() и _safeBatchTransferFrom(), которые с небольшой разницей делают одно и тоже: порождают события о транзакции, вызывают дополнительные служебные функции, которые нам известны с прошлых стандартов, _beforeTokenTransfer() и _afterTokenTransfer(), а также вызывают новую функцию с длинным названием _doSafeTransferAcceptanceCheck(). Но сначала о небольшой разнице между _safeTransferFrom() и _safeBatchTransferFrom(), которая заключается в том, что во второй функции работа идет с массивами, а в первой их нет. И чтобы как-то унифицировать этот процесс, нам нужно аргументы из первой функции переделать в массивы. Для этого используется служебная функция _asSingletonArray(). Простая, но очень полезная. Она принимает число, как аргумент, и возвращает массив. Там просто создается новый массив через new result = uint[](1), так как нужна фиксированная длина, и записывается аргумент - result[0] = el. Далее переходим к той функции с длинным названием, в которой используется подключенный нами ранее интерфейс IERC1155Receiver. В этой функции, мы создаем уловие и проверяем получателя. Если это обычный адрес, то все ок. Если же адрес контракта, то мы через try-catch посылаем запросы. В прошлых описаниях стандартов мы использовали assembly, чтобы получать ответ с ошибкой в try-catch. Но сейчас Solidity позволяет делать это своими средствами. try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns(bytes4 resp) {     if(resp != IERC1155Receiver.onERC1155Received.selector) {      revert("Rejected tokens!");     } } catch Error(string memory reason) {     revert(reason); } catch {      revert("Non-ERC1155 receiver!"); } Если в ответе мы получаем селектор функции, которая равен тому, что мы отправили, то транзакция проходит без проблем. В остальных случаях мы ловим ошибку. В рамках исполнения функции _doSafeTransferAcceptanceCheck() и _doSafeBatchTransferAcceptanceCheck() абсолютно одинаковые. Вот и все! Теперь мы знаем еще и реализацию стандарта ERC1155. #erc1155 #nft

Разбор контракта ERC1155. Часть 1 В принципе, если вы хорошо изучили предыдущие стандарты ERC21 и ERC721, то понимание данного контракта не должно составить каких-либо трудов. Для начала здесь подключаются три интерфейса: import "./IERC1155.sol"; import "./IERC1155MetadataURI.sol"; import "./IERC1155Receiver.sol"; Первый описывает функции нашего контракта, второй дополняет его с реализацией  function uri(), которая возвращает ссылку на токен по его id, а третий - знакомый нам по ERC721, который помогает делать проверку другого контракта на возможность приема токенов. Особенность данного стандарта в том, что тут есть функции, как для передачи одного токена, так и их группы (Batch). Далее вводятся два mapping: одни - на проверку "какое количество данного токена есть на конкретном адресе", и второй - "который говорит, что тот или иной адрес (оператор) имеет право распоряжаться токенами на другом конкректом адресе". И добавляем переменную string, которая будет хранить ссылку на все наши токены. Например, там будет храниться ссылка "https://alltokens.eth/myToken/...", и на месте троеточия будет подставляться id конкретного токена после. Далее в конструкторе принимает строку ссылку и передает ее в функцию setURI(), которая устанавливает новою ссылку от разработчика. Также есть простая функция uri(), которая просто возвращает саму ссылку. balanceOf(), уже знакомая нам, показывает, сколько токенов на определенном адресе. setApprovalForAll() - выдает разрешение на управление нашими токенами оператору через служебную функцию _setApprovalForAll() с дополнительными проверками на владельца, и isApprovedForAll(), соответственно, проверяет это разрешение. Интерес тут представляет balanceOfBatch(), которая в качестве аргументов принимает массивы адресов и id токенов и возвращает массив с их количеством на адресе. Для этого делается проверка, что длина массива с аккаунтами равна длине с id токенов, так как, если, например, количество адресов будет больше, то где-то совершена ошибка. Там же создается новый массив с фиксированной длинной. Мы же помним, что в memory можно создавать только такие массивы? И через цикл мы прогоняем адреса, вызывая функцию balanceOf() для каждого адреса и id токена, записывая все в новый массив, который и возвращаем. Еще нужно отметить, что в ERC1155 решили отказаться от обычной функции transferFrom(), и заменить ее safeTransferFrom(), которая выполняет дополнительные проверки перед пересылкой токенов. #erc1155 #nft

Кратко о различиях ERC721 и ERC1155 Вчера я немного опоздал на стрим и пропустил момент, когда лектор рассказывал о различиях этих двух стандартов. И до сегодняшнего дня меня не покидало ощущение, что я что-то где-то "недогнал". Пересматривая видео, и покопавшись позже в документации и статьях, я чуть лучше разобрался в этой теме. Итак, по сути, ERC1155 - это просто улучшенная версия ERC721. Чтобы создать свой NFT в ERC721 нужно было каждый раз создавать новый контракт, при этом каждый токен мог быть исключительно в одном экземпляре. В ERC1155 можно создавать много токенов в рамках одного контракта, при этом каждый отдельно взятый токен может быть в нескольких экземплярах. Сейчас попробую описать это на примере. Возьмем для примера игру Counter Strike. Это такая стрелялка, где команда спецназа борется с командой террористов, и там существует несколько видов огнестрельного оружия. Так вот, в наши дни это оружие может иметь разный визуальный дизайн, или, как говорят, скин. Одно оружие и, например, 10 разнообразных видов, как оно может выглядеть. Разработчики игры могут вводить эти скины в игру за дополнительную оплату. При этом, они могут выпустить всего 10 скинов - лимитировать спрос. И только 10 человек от всех игроков будут иметь их в своем арсенале. А могут выпустить и 1 уникальный легендарный скин, который может выкупить только один игрок, и потом хвастаться всем остальным, какой он уникальный. Другими словами, в рамках ERC1155 мы можем выпускать как уникальные NFT в единственном экземпляре, так и NFT в 2, 5, 20 экземплярах и более. И все они будут управляться одним нашим контрактом! Именно за свою гибкость и новый подход, ERC1155 уже пришел на смену ERC721. Поэтому, если вы захотите создать свой NFT, то лучше писать его сразу на ERC1155. #erc1155 #nft

Урок 27 - ERC1155: NFT и взаимозаменяемые токены Ну, вы, конечно, монстры! Я думал, что после вчерашнего залпа постов в течение дня, с канала отпишутся несколько человек. Но все на месте! Это очень круто! Сегодня мы разберем вчерашний стрим от Ильи, а с этим и закончим проходить его уроки. Да, вы можете заметить некоторое несоответствие в нумерации, типа у нас сейчас 27, а на канале уже 31. Объясняю: два урока однажды у нас проходили как один, видео с вопросами для собеседования вторая часть будет выложена завтра, как подведение итогов, и два видео про фронт Next я специально пропустил, так как позже будем отдельно разбираться с этой темой. Вот и выходит все 31 урок. Новое видео про ERC1155. Ну, что же, приятного просмотра и легкого обучения! #урок #erc1155 #nft

Используйте адрес типа интерфейса вместо простого типа адреса Для большей безопасности в контракте следует использовать interface type вместе обычного типа address в аргументах функции. contract Validator {     function validate(uint) external returns(bool); } contract TypeSafeAuction {       // good     function validateBet(Validator _validator, uint _value) internal returns(bool) {         bool valid = _validator.validate(_value);         return valid;     } } contract TypeUnsafeAuction {       // bad     function validateBet(address _addr, uint _value) internal returns(bool) {         Validator validator = Validator(_addr);         bool valid = validator.validate(_value);         return valid;     } } #hint #tx #tx.origin

Избегайте tx.origin Никогда не используйте tx.origin для проверок или авторизации, например так "require(tx.origin == owner)". В этом случае другой контракт (хакер) может получить доступ к вашему контракту и вывести все деньги. Вместо этого используйте msg.sender. Более того, tx.origin может быть выведен из языка в последующих обновлениях Solidity. #hint #tx #tx.origin

Аккуратнее со встроенными функциями В Solidity существуют встроенные функции, которые доступны в написании контракта по умолчанию, как например revert() или selfdestruct(). Так вот, если вы работаете с контрактом, который наследует от другого (других) контракта, то вам следует проверять все функции, которые наследуются. Есть некоторый шанс, что вы наткнетесь на функции похожие на встроенные, но с другим исполнением. contract PretendingToRevert {     function revert() internal {} } contract ExampleContract is PretendingToRevert {     function somethingBad() public {         revert();     } } В примере, вызов функции revert() выполнит не откат транзакции, как это должно быть, а условие из PretendingToRevert. Будьте внимательны и всегда проверяйте исходный код наследуемых контрактов. #hint #build-in

Используйте event для мониторинга Иногда бывает, что контракт может принять деньги, но будет не понятно откуда и когда они пришли, как например при наследовании одного контракта другим. contract Charity {     mapping(address => uint) balances;     function donate() payable public {         balances[msg.sender] += msg.value;     } } contract Game {     function buyCoins() payable public {         // 5% goes to charity         charity.donate.value(msg.value / 20)();     } } Когда контракт Game сделает вызов функции Charity.donate(), эта транзакция не отобразится во внешних транзакциях Charity. Именно для таких целей лучше всего использовать event, которые будут порождать события при совершении переводов. #hint #event

Версия pragma Solidity В начале каждого файла нашего контракта мы указываем версию pragma. Другими словами, мы сообщаем версию языка Solidity, с которой работали. Так вот, не уверен, насколько этот совет из статьи актуален сейчас, однако звучит достаточно здраво: фиксируйте версию pragma для своего контракта. Если вы заметили, то мы обычно пишем так: "pragma solidity ^0.8.0;". И вот этот значок "^" указывает на то, что для контракта могут подходить версии 0.8.0 и выше. И данная рекомендация, предлагает писать без этого знака, как бы говоря, что мы работаем только с этой версией Solidity. Это может стать актуальным если, скажем, версия 0.9.0 введет изменения в языке и функциях, и тогда наши контракты будут выдавать ошибки, если указан "^". Повторяю, не знаю, насколько это правильно в текущих реалиях, но доля логики здесь есть. #hint #pragma

Особенности модификатора payable Если функция, не помеченная как payable, вызывается в другой функции помеченной как payable, то транзакция все равно сможет передать эфир, так как msg.value будет установлен. #hint #payable

Особенности fallback функций Если fallback() указан как payable, то он может принимать Ether. Особенность отправляющих Ether функций transfer и send в том, что у них есть ограничение - инициированная ими транзакция не должна расходовать больше, чем 2300 gas. Поэтому, если внутри fallback реализована какая-то сложная логика (вызвать еще какие-то функции, записать storage и т.д.) (при поступлении Ether с помощью transfer или send), то это будет стоить больше, чем 2300 и транзакция откатится. Максимум на что хватит газа в такой ситуации внутри fallback - это на emit event. При этом, если эфир отправляется с помощью call вызовов, то есть возможность повышения лимита газа, и тогда в fallback функция не откатит транзакцию. Будьте аккуратны с этим, и всегда дополняйте fallback другими функциями, которые могут принимать деньги, например receive. #hint #fallback #receive

Интерфейсы и абстрактные контракты Интерфейсы и абстрактные контракты призваны помочь с написанием кода нашего контракта и облегчить его. Однако следует помнить, что интерфейсы не могут выполнять функции, не имеют доступа к storage и не могут наследовать от других интерфейсов. При этом абстрактные контракты так делать могут, что делает их более универсальными. Но нужно помнить, что при наследовании нашим контрактом абстрактного, необходимо override его функции. #abstract #interface #hint

Аккуратнее с делением чисел Solidity, на данный момент сентября 2022 года, не поддерживает числа с точкой, и при делении 5/2 будет показан результат "2". Т.е. вместе с откидыванием цифр после точки, он еще и округляет результат до меньшего числа. Это действительно проблема для большинства разработчиков. И многие пытаются преодолеть ее через дополнительные библиотеки на openzeppelin или пишут свои "костыли". В документации по Solidity пишут, что нужно использовать мультипликатор, как в примере: uint multiplier = 10; uint x = (5 * multiplier) / 2; Из других статей я понял, что можно также выполнять все математические операции на фронтенде, чтобы все было точнее, а уже результат сохранять в Solidity. Некоторые предлагают использовать decimals эфира, но я не видел хороших примеров. #division #integer #hint

Используйте модификаторы правильно Интересное замечание, которое я не встречал еще в практике. Не рекомендуется использовать модификаторы, в которых изменяются переменные состояния или реализуются внешние вызовы, например при наследовании, так как сами модификаторы исполняются до кода в функции. contract Registry {     address owner;     function isVoter(address _addr) external returns(bool) {         // Code     } } contract Election {     Registry registry;     modifier isEligible(address _addr) {         require(registry.isVoter(_addr));         _;     }     function vote() isEligible(msg.sender) public {         // Code     } } Например, выше вы можете видеть НЕ правильное использование модификатора, так как контракт Registry может делать reentrancy атаку в другом контракте, вызывая Election.vote() внутри isVoter(). Модификаторы чаще всего используются, чтобы заменить дублирующийся код в функциях, по примеру проверки владельца isOwner(). #modifier #hint

Используйте assert() и require() правильно Assert() следует использовать только в тестах внутренних (internal) ошибок или для проверки инвариантов. Require() используют для проверки условий. #assert #require #hint

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

Solidity by Example Сегодня, в ожидании стрима, на канале не будет уроков. Вместо этого я сделаю несколько полезных постов по работе с Solidity. Я нашел один интересный сайт, где приводятся примеры кода Solidity, как шпаргалки. Очень удобно, например, когда вы забыли какой-то урок или функцию, и хотите быстро по коду понять написание или использование. Да, он на английском, но тем не менее очень понятен интуитивно. Solidity by Example. #links #hint

Тесты с урока про Honeypot Также хочу обратить ваше внимание, что лектор в уроке для проведения тестов с контрактами использует деплой с учетом последних обновлений hardhat, который я описывал ранее. Однако напомню для повторения. В начале он импортирует loadFixture, expect и ethers для проведения тестов, а также type для использования typechain. import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; import { ethers } from "hardhat"; import type { Bank, Attack, Logger, Honeypot } from "../typechain-types"; Затем в деплое, вместо beforeEach, пишет функцию dep(), которая возвращает объекты для тестирования ниже. И в тестах начинает работы с получения этих объектов через await loadFixture(dep). На данный момент это стандарт работы с тестами в hardhat и их нужно знать. #honeypot #deploy

Разбор Honeypot Для того, чтобы вы понимали лучше разбор данного урока, я скидываю ссылку на комит урока от лектора. Лучше всего будет, если вы прочитаете пост, а затем еще раз сами просмотрите код контрактов. Как я понял из урока, honeypot основан на подмене контракта для логирования событий в контракте банка. Вроде как, всем пользователям показывается первый обычный контракт Logger, который просто порождает событие для log, а на самом деле, наследование идет от другого контракта, который был как бы спрятан от глаз пользователей. И если не разобраться, от кого идет наследование в ILogger public logger в контракте банка, то можно попасть на деньги. Также в данном примере была использована уязвимость reentrancy (по сути, повторный возов функций кода) в контракте банка, чтобы заманить в ловушку потенциального хакера. Хакер думает, что раз "balances[_initiator] = 0" вызывается после отправки средств на счет пользователя, то можно создать новый контракт для атаки, который будет вызывать функцию _withdraw каждый раз по новому пока на счету банка не останется средств. И вот для того, чтобы обмануть хакера, который хочет обмануть нас, в контракте банка мы создаем новую переменную "bool resuming", чтобы запутать хакера, и в функции _withdraw принимаем дополнительный аргумент "uint _statusCode". После этого, уже как бы мы сами создаем контракт honeypot и обновляем withdraw с новой bool переменной. Т.е. изначально лектор показывал, что honeypot создал мошенник, чтобы обманывать пользователей, а позже honeypot создаем мы, чтобы обмануть мошенника. Уже в нашем honeypot мы используем "_actionCode", чтобы определить, кто пытается вызвать withdraw. Если обычный пользователь снимает свои деньги, то все проходит ок. Если же мошенник пытается использовать уязвимость reentrancy и вызывать повторно withdraw, то через нее мы вызываем служебную _widthdraw с новым передаваемым аргументом "_statusCode", который на втором вызове меняет значение на "2". Теперь сам мошенник попадает в нашу ловушку honeypot, транзакция откачивается и он не может получить даже свои деньги. Вот как-то так. В 4 контрактах и 1 интерфейсе из урока легко запутаться, полагаю как и в данном посте при первом прочтении. Тут главное понять, что в рамках урока мы действуем и как мошенник и как тот, кто хочет обмануть мошенника, поэтому один код работает сразу для двух примеров. #honeypot

Что такое Honeypot И по традиции, давайте разберемся, что же такое этот Honeypot. Honeypot (горшок с медом) - это некая ловушка для пользователей, которая основана на не самых очевидных моментах в коде. Чаще всего им пользуются мошенники, чтобы обманом продать токены, которые нельзя будет потом перепродать. Допустим, по всем каналам в Телеграм и на популярных ресурсах кто-то делает посты о том, что он запускает крутые токены в оборот, что у него большие планы, и уже несколько крупных компаний поддерживают его инициативу. Конечно же, он предлагает всем пользователям купить его токены по бросовой цене в 0.1 $ и обещает, что через полгода их стоимость будет около 1 $ или даже 5 $. Доверчивые люди бросаются покупать токены в надежде заработать. Но через указанный промежуток времени, никакого роста токена нет, и пользователи решают продать его. А не тут-то было! Токены продать больше нельзя, и мошенник уходит с деньгами создавать новый скам. В этом уроке и обращают наше внимание на подобные нюансы в коде, как избежать обмана, и как перехитрить мошенника, если он захочет использовать уязвимость нашего контракта на reentrancy. #honeypot