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

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

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

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

Новый видео урок.

Также напоминаю, что завтра у нас будет стрим от Ильи в 19:00 по московскому времени на его канале. Можно будет узнать новую тему, которую мы будем разбирать в пятницу, и задать вопросы по Solidity.

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

На этой неделе мы закончим продвинутое обучение и со следующей я расскажу вам о новых планах.

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

#урок #honeypot
Что такое Honeypot

И по традиции, давайте разберемся, что же такое этот Honeypot.

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

Допустим, по всем каналам в Телеграм и на популярных ресурсах кто-то делает посты о том, что он запускает крутые токены в оборот, что у него большие планы, и уже несколько крупных компаний поддерживают его инициативу. Конечно же, он предлагает всем пользователям купить его токены по бросовой цене в 0.1 $ и обещает, что через полгода их стоимость будет около 1 $ или даже 5 $. Доверчивые люди бросаются покупать токены в надежде заработать. Но через указанный промежуток времени, никакого роста токена нет, и пользователи решают продать его.

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

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

#honeypot
👍1
Разбор 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

Также хочу обратить ваше внимание, что лектор в уроке для проведения тестов с контрактами использует деплой с учетом последних обновлений 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
Solidity by Example

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

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

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

Да, он на английском, но тем не менее очень понятен интуитивно.

Solidity by Example.

#links #hint
Статья с рекомендациями

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

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

#link #hint
Используйте assert() и require() правильно

Assert()
следует использовать только в тестах внутренних (internal) ошибок или для проверки инвариантов.

Require() используют для проверки условий.

#assert #require #hint
👍1
Используйте модификаторы правильно

Интересное замечание, которое я не встречал еще в практике.

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

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
👍1
Аккуратнее с делением чисел

Solidity, на данный момент сентября 2022 года, не поддерживает числа с точкой, и при делении 5/2 будет показан результат "2". Т.е. вместе с откидыванием цифр после точки, он еще и округляет результат до меньшего числа.

Это действительно проблема для большинства разработчиков. И многие пытаются преодолеть ее через дополнительные библиотеки на openzeppelin или пишут свои "костыли".

В документации по Solidity пишут, что нужно использовать мультипликатор, как в примере:

uint multiplier = 10;
uint x = (5 * multiplier) / 2;

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

Некоторые предлагают использовать decimals эфира, но я не видел хороших примеров.

#division #integer #hint
👍1
Интерфейсы и абстрактные контракты

Интерфейсы и абстрактные контракты призваны помочь с написанием кода нашего контракта и облегчить его.

Однако следует помнить, что интерфейсы не могут выполнять функции, не имеют доступа к storage и не могут наследовать от других интерфейсов.

При этом абстрактные контракты так делать могут, что делает их более универсальными. Но нужно помнить, что при наследовании нашим контрактом абстрактного, необходимо override его функции.

#abstract #interface #hint
👍1
Особенности 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
Особенности модификатора payable

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

#hint #payable
Версия pragma Solidity

В начале каждого файла нашего контракта мы указываем версию pragma. Другими словами, мы сообщаем версию языка Solidity, с которой работали.

Так вот, не уверен, насколько этот совет из статьи актуален сейчас, однако звучит достаточно здраво: фиксируйте версию pragma для своего контракта.

Если вы заметили, то мы обычно пишем так: "pragma solidity ^0.8.0;". И вот этот значок "^" указывает на то, что для контракта могут подходить версии 0.8.0 и выше.

И данная рекомендация, предлагает писать без этого знака, как бы говоря, что мы работаем только с этой версией Solidity.

Это может стать актуальным если, скажем, версия 0.9.0 введет изменения в языке и функциях, и тогда наши контракты будут выдавать ошибки, если указан "^".

Повторяю, не знаю, насколько это правильно в текущих реалиях, но доля логики здесь есть.

#hint #pragma
Используйте 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
👍1
Аккуратнее со встроенными функциями

В Solidity существуют встроенные функции, которые доступны в написании контракта по умолчанию, как например revert() или selfdestruct().

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

contract PretendingToRevert {
    function revert() internal {}
}

contract ExampleContract is PretendingToRevert {
    function somethingBad() public {
        revert();
    }
}

В примере, вызов функции revert() выполнит не откат транзакции, как это должно быть, а условие из PretendingToRevert.

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

#hint #build-in
Избегайте tx.origin

Никогда не используйте tx.origin для проверок или авторизации, например так "require(tx.origin == owner)"
. В этом случае другой контракт (хакер) может получить доступ к вашему контракту и вывести все деньги.

Вместо этого используйте msg.sender.

Более того, tx.origin может быть выведен из языка в последующих обновлениях Solidity.

#hint #tx #tx.origin
Используйте адрес типа интерфейса вместо простого типа адреса

Для большей безопасности в контракте следует использовать 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
Урок 27 - ERC1155: NFT и взаимозаменяемые токены

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

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

Да, вы можете заметить некоторое несоответствие в нумерации, типа у нас сейчас 27, а на канале уже 31. Объясняю: два урока однажды у нас проходили как один, видео с вопросами для собеседования вторая часть будет выложена завтра, как подведение итогов, и два видео про фронт Next я специально пропустил, так как позже будем отдельно разбираться с этой темой. Вот и выходит все 31 урок.

Новое видео про ERC1155.

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

#урок #erc1155 #nft
👍1
Кратко о различиях 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
👍1
Разбор контракта 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
Разбор контракта 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