TGViewer
Из Solidity в AI и дальше Из Solidity в AI и дальше @solidityset · 2.48K subscribers
Post #856 820
Реентранси, рекурсия и ресмысление...

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

И вот вчера, меня немного поставили в ступор простым кодом:

contract Wallet {

mapping(address => uint) public balances;

function deposit(address _to) public payable {
balances[_to] = balances[_to] + msg.value;
}

function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}

function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
require(result, "External call returned false");
balances[msg.sender] -= _amount;
}
}

receive() external payable {}
}

contract AttacWallet {
uint constant SUM = 1 ether;
Wallet walletRe;

constructor(address payable _wallet) {
walletRe = Wallet(_wallet);
}

function depositAttack() public payable {
walletRe.deposit{value: SUM}(address(this));
}

function attack() external payable {
walletRe.withdraw(SUM);
}

receive() external payable {
if(address(walletRe).balance >= SUM) {
walletRe.withdraw(SUM);
}
}
}

Есть два контракта: Wallet и AttacWalet, второй предназначен для взлома первого.

Вопрос был простой: "Почему не работает реентранси?".

И код написан правильно. Но вот, почему не проходит...

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

balances[msg.sender] -= _amount;

было

balances[msg.sender] = 0;

И вот если в данном контракте также обнулять контракт, то атака работает!

Мне потребовалось некоторое время и помощь коллег, чтобы разобраться с этим.

Скажу сразу, я не правильно понимал ход выполнения атаки реентранси. До сего дня я полагал, что функция будет заходить в withdraw(), доходить до call вызова, перебрасываться в контракт хакера в receive(), а оттуда снова в withdraw() и так по кругу, пока не будет выполнено условие if(address(walletRe).balance >= SUM) {}, после чего call наконец пройдет, и исполнение функции дойдет до balances[msg.sender] -= _amount.

Короче, это не так.

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

Лучше всего о рекурсии можно узнать из этого видео, спасибо @elawbek за наводку!

Смотрите, что получается. Допустим у нас есть 10 Эфиров на контракте Wallet, которые мы хотим увести. На контракте хакера 1 Эфир для атаки.

Сначала мы делаем депозит и переводим 1 Эфир с контракта хакера, на контракт Wallet, и так теперь 11 Эфира. После чего мы запускаем функцию attack().

Мы попадаем в withdraw(), происходит рекурсия, которая под капотом исполняет данную функцию 11 раз, так как всего там 11 Эфира лежит. И когда рекурсия начинает "сворачиваться", то оказывается, что успешная пересылка Эфира прошла всего один раз, и тогда уже сработало balances[msg.sender] -= _amount. В каждый последующий раз мы вычитали 1 Эфир из balances[msg.sender], на котором уже был ноль. Происходило переполнение и call вызов возвращал "External call returned false".

Именно поэтому атака реентранси тут и стопорилась!

Но почему в некоторых статьях также можно встретить описание атаки, где присутствует balances[msg.sender] -= _amount? Как, например, тут.

Объяснение просто: все дело в pragma и версии Solidity. До 0.8 версии в математических расчетах не было проверки на overflow. Система не выдавала ошибку, когда программа пыталась вычесть какое-либо число из 0.

Кстати, задача Ethernaut именно на этом и построена!
  • 👍 9
  • ❤ 2
  • 👏 2
  • 😁 1
More from @solidityset
  1. Oct 2, 2026Вторая сложность — как доказать, что установилось именно опубликованное. В первом варианте…
  2. Oct 2, 2026Задача с полуоткрытым кодом На днях, в процессе создания одного приложения, столкнулся с и…
  3. Sep 22, 2026Какой язык программирования учить сейчас? На днях в Твиттере увидел небольшой пост о разви…
  4. Sep 18, 2026Интересная модель Jev Буквально пару дней назад в Твиттере многие начали обсуждение новой…
  5. Sep 14, 2026Графы повсюду Если вы также следите за новостями в мире ИИ, то наверняка уже все чаще встр…
  6. Sep 10, 2026GTA6, Cyberleek, блокчейн и безопасность Увидел несколько постов (тут и тут) про Cyberleek…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →