Solidity. Смарт контракты и аудит
2.63K subscribers
246 photos
7 videos
18 files
555 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
Безопасность. Внешние вызовы. Часть 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
Solidity hints. Часть 13

Понемногу движемся дальше и сегодня на очереди у нас:

19. Internal function calls do not create an EVM message call. They are called using simple jump statements. Same for functions of inherited contracts.

что в переводе:

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

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

У нас есть 4 области видимости для функций: public, external, internal и private. Первые две - открытые для доступа других контрактов, вторые - закрыты.

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

Доступ к внутренним функциям возможен только внутри текущего контракта или контрактов, вытекающих из него. Они не могут быть доступны извне. Поскольку они не открыты для внешнего доступа через ABI контракта, они могут принимать параметры внутренних типов, таких как mapping или storage references.

Внутренние функции вызываются через инструкции опкоды JUMP / JUMPI, просто перепрыгивая в другую точку текущего кода. Это имеет смысл, потому что внутренние функции не меняют контекст (то есть остаются внутри одного и того же контракта при вызове).

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

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

Следующий пункт:

20. If you have a public state variable of array type, then you can only retrieve single elements of the array via the generated getter function.

что в переводе и полной версии из документации звучит как:

Если у вас есть публичная переменная состояния типа массив, то вы можете получить только отдельные элементы массива через сгенерированную функцию getter. Этот механизм существует для того, чтобы избежать высоких газовых затрат при возврате целого массива. Вы можете использовать аргументы, чтобы указать, какой отдельный элемент возвращать, например myArray(0). Если вы хотите вернуть весь массив за один вызов, то вам нужно написать функцию, например:

contract arrayExample {

uint[] public myArray;
function myArray(uint i) public view returns (uint) {
return myArray[i];
}

function getArray() public view returns (uint[] memory) {
return myArray;
}
}


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

Смотрите, когда мы создаем переменную с областью видимости public, EVM также создает для нее специальный геттер, по которому можно прочитать значение этой переменной. Проще всего это будет понять, попытавшись создать в Ремиксе контракт с двумя переменными public и internal.

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

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

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

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

#array #getter #jump #external
👍2💯2🌭1
Атака Denial-of-Service. Часть 6

Безопасно ли протокол обрабатывает взаимодействия с внешними контрактами?

Проблема: многие смарт-контракты полагаются на взаимодействия с внешними контрактами. Неожиданное поведение внешних контрактов может привести к сбою всей системы. Неспособность обработать эти внешние ошибки приводит к уязвимости DoS.

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

Пример:

Рассмотрим контракт, который взаимодействует с внешним фидом цен Chainlink. Без надлежащей обработки ошибок с помощью try/catch любой откат транзакции из внешнего фида будет каскадироваться вверх, и весь вызов будет ревертиться.

contract PriceDependentContract {
AggregatorV3Interface public priceFeed;

constructor(address _priceFeed) {
priceFeed = AggregatorV3Interface(_priceFeed);
}

// Vulnerable function that retrieves the price without handling potential Chainlink reverts
function getPrice() public view returns (uint256) {
(, int256 price, , , ) = priceFeed.latestRoundData(); // Vulnerable line: No error handling
require(price > 0, "Price must be positive");
return uint256(price);
}

function calculateSomethingImportant() public view returns (uint256) {
uint256 price = getPrice();
// ... some important calculation using the price
return price * 2;
}


Как исправить: оберните вызовы внешних контрактов в блоки try/catch для обработки возвращенных ошибок и реализуйте резервный вариант или кэшированное значение.

Примечание: существует особый случай, когда внешний контракт намеренно тратит газ, что может привести к сбою блока catch! Мы обсудим это позже.

#dos #external
👍4