امنیت در قرارداد هوشمند (Smart Contract Security)

امنیت، واژه ای مهم در همه زمینه ها. توی دنیای امروز با پیشرفت دیوانه کننده تکنولوژی بیشتر از هر وقت دیگه ای به امنیت تو زمینه نرم افزار و وب احساس میشه که قطعا هم قرارداد های هوشمند هم از این قاعده مستثنی نیست.
در قرارداد های هوشمند امنیت به معنی واقعی کلمه معنی پیدا میکنه چرا که یک قرارداد بعد از ثبت در بلاکچین امکان ویرایش یا حذف آن وجود نداره (راه هایی هست که بشه یک قرار داد رو حذف کرد مثل استفاده از تابع ()selfdestruct) پس یک اشتباه در نوشتن یک متغیر و یا یک محاسبه ی ساده ممکنه قرار داد رو با خطر های جبران ناپذیری مواجه کنه.
در این قسمت قصد دارم به اصولی که باعث ضعف امنیتی در قرارداد های هوشمند میشه نگاهی بندازیم که رعایت کردنشون از خود نوشتن کد اهمیت بیشتری داره.

Best Practices / بهترین روش ها

مثل تمام زبان های برنامه نویسی، قرارداد های هوشمند هم همان چیزی که ما نوشتیم رو اجرا میکنه ولی با این تفاوات مهم که بعد از ثبت در بلاکچین قابل تغییر نیست!
با این وجود رعایت اصول یا شیوه های اولیه درست در نوشتن قرارداد هوشمند میتونه تا حدودی به
بالا بردن سطح امنیت و خوانایی کدهای ما کمک کنه.

ساده نویسی

همشیه هرچی ساده تر بهتر ! از پیچیدگی زیاد دوری کن.

کیفیت کدها

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

تست کردن کدها

همیشه کدها رو تست کنید حتی ساده ترین متغیر های برنامه رو چندین بار تست کنید و از درست بودن عملکرد کلی اطمینان حاصل کنید.

خوانایی

درسته قبلا هم به این موضوع اشاره کوچکی کردم ولی واقعا موضوع مهمیه.کدها باید ساده، مختصر و مفید باشن و از پیچیدگی زیاد که سودی جز سردرگمی نداره جلوگیری کنید.

overflow and underflow / سر ریز و ته ریز

اول از هر چیزی بیایم ببینیم overflow و underflow به چه معنی هستن.
EVM (ماشین مجازی اتریوم)، برای هر داده ی عددی مقدار یا اندازه مشخصی در نظر گرفته که میتونه توی خودش ذخیره کنه . مثلا uint8 فقط توانایی ذخیره عدد در بازه [255 , 0] رو داره پس اگر سعی کنیم که در این نوع متغیر تا 256 ذخیره کنیم (256 = 8 ^ 2) صفر به ما برمیگردونه. اضافه کردن اعداد بزرگتر از محدوده نوع داده رو overflow و کم کردن یک نوع داده از عددی کوچکتر از محدوده داده رو با underflow میشناسن.
حالا چرا این موضوع خطرناکه؟ چون یک اشتباه کوچک در محاسبه ممکنه خطر و مشکلات زیادی رو با خودش داشته باشه به ویژه در پروژه های صرافی، که داده های عددی و جمع و تفریق روی انها نتجش میشه دارایی ما یا مقدار درست توکنی که قصد داریم برداشت یا واریز کنیم.
راه حل چیه ؟ بهترین راه حل برای این موضوع که تا حد بسیار زیادی از بروز این مشکلات جلوگیری میکنه استفاده کردن از کتابخانه های موجوده مثل OpenZeppelin که محاسبات عددی مثل ضرب و تقسیم و … رو با کمترین درصد خطا انجام میده و ریسک به وجود اومدن اشتباهات محاسباتی رو تا حد زیادی پایین میاره.

Default Visibilities / قابلیت دیده شدن پیش فرض

فانکشن ها در قراد داد هوشمند قابلیت تعیین روش دسترسی دارند.به عنوان مثال فانکشن های public یا عمومی و یا فانکشن های private یا خصوصی.
توابع به صورت پیشفرض public هستن، یعنی از بیرون قرارداد هوشمند، توسط کاربر و یا یک قرارداد هوشمند دیگه قابل دسترسی هستن.این موضوع میتونه امنیت قرارداد هوشمند مارو تا حد زیادی تحدید کنه.
راه حل چیه ؟ بهترین روش برای جلوگیری از حمله ها و خطرها از این طریق اینه که همیشه برای فانکشن های یک قرارداد هوشمند visibility یا میان دید تعیین کنیم و دفت کنیم کدام فانکشن باید توسط کاربر قابلیت صدا زدن و دسترسی داشته باشه و کدام فقط و فقط درون و به وسیله خود قرارداد هوشمند.

Entropy Illusion / توهم آنتروپی

در بلاکچین اتریوم عملیات به هیچ وجه تصادفی نیست ! این به این معناست که همه ی معاملات در global state اکوسیستم اتریوم بدون هیچ ابهام و به طور قابل محاسبه ای تغییر میکنه.این به این معنیه که هیچ منبع آنتروپی یا تصادفی در اتریوم وجود نداره.
دست یابی به آنتروپی غیرمتمرکز یک مشکل شناخته شده ایه که راه حل هایی از جمله استفاده از زنجیره ای از هش ها دربارش پیشنهاد شده و افراد زیادی از جمله Vitalik Buterin به آن تاکید کرده.
مشکل اصلی و مهم در این زمینه تخمین زدن بلاک ها توسط minerهاست، که اطلاعاتی از جمله GasLimit , block number , … رو پیش بینی میکنند که به صورت تصادفی نیست.
بسیاری از پروژه هایی وجود دارن که با یک روش نادرست یک سیستم تصادفی به وجود اوردن که همونطور که گفتیم ممکنه توسط minerها قابل حدس زدن باشه.
راه حل چیه؟ بهترین راه حل تا این زمان استفاده از Oracle هاست.که از بیرون بلاکچین تصادفی بودن رو برای دنیای درون بلاکچین فراهم میکنند.

External Contract Referencing / ارجاع قرارداد های خارجی

یکی از مزایای بلاکچین اتریوم قابلیت استفاده مجدد از کد و تعامل با قرارداد هایی است که قبلا در شبکه ثبت شدن و موجود هستن.
درنتیجه تعداد زیادی از قرارداد ها با استفاده از صدا زدن قراردادهای موجود در شبکه به صورت خارجی تعامل میکنن که این موضوغ میتونه باعث سواستفاده افراد سودجو و مخربی میشه که با روش های غیر مشخص کد های خودشون رو پنهان میکنن.
همانطوری که گفتیم فرارداد های ایمن در برخی موارد میتوانند به گونه ای اجرا شوند که رفتار مخرب داشته باشند.یک توسعه دهنده میتونه یک قرارداد عمومی رو تایید کنه و از آن استفاده کنه ولی درحالی که یک قرارداد مخرب عمومی ای که دارای خطرات و ریسک های مخرب هست رو ایجاد کرده.
مهمترین راه برای جلوگیری از این اتفاق استفاده از کلمه کلیدی New برای ایجاد قرارداد هاست.
با این روش ما یک نمونه از قرارداد مستقر در شبکه نمونه برداری یا کپی میکنیم که این عمل باعث میشه توسعه دهنده قراردادی که ما استفاده میکنیم امکان جایگزینی قرارداد به صورت مخفی را بدون تغییر نسخه اصلی آن نداشته باشه.
روش بعدی استفاده از آدرس قرارداد به صورت Hard Code.
به طور کلی باید حواسمون به کدها و قرارداد های خارجی ای که استفاده میکنیم باشه با دقت متغیر ها و فانکشن هارو بررسی کنید و چک کنید که کدام یک به صورت خصوصی یا Private تعریف شده و چرا.

Unchecked CALL Return Values / عدم بررسی نتیجه Call ها

روش های مختلفی برای ارتباط با یک قرارداد خارجی وجود داره.برای ارسال اتر به حساب های خارجی معمولا از فانکشن Send استفاده میشه . این تابع نتیحه رو به صورت Bool یا (True , False) برمیگردونه که آیا نتیحه انتقال موفقیت آمیز بوده یا خیر.این تابع دقیقا مثل یک هشدار عمل میکنه به این صورت که ارزش یا value یک تراکنش که تابع رو اجرا کرده در صورت شکست بازنمیگرده و تابع به سادکی False برمیگردونه.
یک اشتباه رایج اینه که توسعه دهنده ها انتظار دارن که اگر عملیات با شکست مواحه شد تراکنس به اصطلاح Revert بشه یا برگردانده بشه.که در صورت برگشتن، value هم برمیگرده و خطری نداره ولی همانطور که گفتم اینطور نیست!
راه حلش چیه؟ سعی کنید تا جایی که ممکنه از تابع Transfer به حای Send استفاده کنید.به این دلیل که در صورت شکست معامله به هردلیلی value یا به طور ساده تر دارایی شما هم برمیگرده یا Revert میشه.اگر هم در شرایطی مجبور به استفاده از تابع Send شدید، حتما مقدار بازگشتی رو از قبل براش برنامه ریزی کنید و همه ی ابعاد ماجرا رو درنظر بگیرید.

Race Conditions / وضعیت مسابقه ای

مانند اکثر بلاکچین ها، Node ها یا گره ها تراکنش ها رو جمع آوری و تبدیل به بلاک میکنند.
معاملات تنها وقتی معتبر به حساب میان که miner ها یک مکانیسم اجماع رو حل کنن.miner ای که بلاک را حل کرده میتونه انتخاب کنه که کدام تراکنش از Transaction pool یا استخر تراکنش ها در بلاک ثبت میشه. این یک زنگ خطره ! چون فردی که قصد حمله داره میتونه تراکنش هایی که ممکنه حاوی یک اطلاعات خاص و مفید باشه رو رصد کنه و با تغییر State، تراکنش رو لغو و یا وضعیت قرارداد رو به ضرر عمل خاص تراکنش تغییر بده.
Attacker میتونه داده ها رو از تراکنش بگیره و تراکنش جدیدی از خودش با GasPrice بیشتر بسازه.به این صورت تراکنش فرد مهاجم به دلیل GasPrice بیشتر در بلاک قبل از تراکنش اصلی لحاظ میشه.
به طور کلی دو گروه از افراد وجود داره که ممکنه به تراکنش ها حمله کنن:

1.افراد عادی (فردی که برای تراکنش های خودش GasPrice رو دستکاری میکنه)

2.miner ها (توانایی رصد کردن تراکنش هایی که مناسب بلاک هستند رو دارن )

گروه اول بسیار خطرناک تره! چرا که miner ها فقط زمانی میتونن تدارک حمله ببینن که بلاک رو حل کرده باشن و بعید هم به نظر میرسه که miner ها یک بلاک مشخص رو هدف قرار بدن.
راه حل این مشکل چیه؟ بهترین روش برای جلوگیری از این مشکل استفاده از GasPrice بالا هنگام انجام تراکنش هاست تا مانع از افزایش بیش از حد و غیر منطقی آن توسط افراد بشه. این ترفند فقط برای گروه اول عمل میکنه و برای miner ها بی تاثیره چونکه miner ها کماکان میتونن تراکنش هارو رصد و تراکنش مورد نظر خودشونو صرف نظر از مقدار GasPrice تراکنش ها انجام بدن .
یک روش عالی و باحال اینه که از ساختار یا الگویی به نام commit–reveal استفاده کنیم. این ساختار به این شکله که ترانش هارو به دو مرحله تقسیم میکنه، مرحله اول به این شکله که تراکنش های کاربران به هش تبدیل میشه و بعد از اینکه تراکنش در بلاک ثبت شد کاربر با انجام یک تراکنش اطلاعات رو فاش یا قابل دیدن میکنه(این مرحله به مرحله افشا یا (the reveal phase) هم معروفه.
این روش از شیطنت هر دو دسته جلوگیری میکنه، بدلیل اینکه امکان تخمین داده های تراکنش ها وجود نداره و این عالیه.ولی باز هم این روش خالی از ایراد نیست، بدلیل اینکه در این روش transaction value یا ارزش تراکنش مخفی نمیشه ! که خیلی هم چیز خوبی نیست.
یک قرارداد هوشمند به نام ENS وجود داره که این امکان رو به کاربر میده که در تراکنش ها مقدار اتری رو که تمایل داره ارسال یا خرج کنه رو هم در داده های تراکنش قرار بده.بدین ترتیب کاربران این امکان رو دارن که تراکنش هایی با ارزش دلخواه درست کنن که در مرحله افشا تفاوت بین مبلغ ارسال شده در معامله و مبلغی که آنها مایل به خرج کردنش بودن به کاربران بازگردانده میشه.

Denial of Service (DoS) / قطع خدمات

این مورد حملات که به DoS معروفه بسیار گستردست، ولی به طور خلاصه اساسا شامل حملاتیه که کاربران بنا به دلایلی میتونن قرارداد رو برای مدتی غیر فعال کنن یا در برخی موارد برای همیشه از کار بندازن. در مورد این حملات، امکان به دام انداختن همیشگی اتر های ارسال شده به قرارداد هم وجود داره.
این Attack ها معمولا به خاطر اشتباهات کوچک در نوشتن قرارداد هوشمند اتفاق میوفته.به عنوان مثال :

contract DistributeTokens {
    address public owner; // gets set somewhere
    address[] investors; // array of investors
    uint[] investorTokens; // the amount of tokens each investor gets

    // ... extra functionality, including transfertoken()

    function invest() external payable {
        investors.push(msg.sender);
        investorTokens.push(msg.value * 5); // 5 times the wei sent
        }

    function distribute() public {
        require(msg.sender == owner); // only owner
        for(uint i = 0; i < investors.length; i++) {
            // here transferToken(to,amount) transfers "amount" of
            // tokens to the address "to"
            transferToken(investors[i],investorTokens[i]);
        }
    }
}

در این قرارداد زمانی این مشکل به وجود میاد که کاربر علاقه داره از فانکشن ** ()distribute** استفاده کنه.در این قرارداد و در فانکشن ()distribute حلقه ای وجود داره که روی آرایه investor عملیات انجام میده که باعث artificialy inflated یا به طور ساده تر حجیم شدن آرایه investorمیشه.
فردی که قصد داره به قرارداد حمله کنه تعداد زیادی اکانت درست میکنه که باعث میشه آرایه investor به مرور بزرگ و بزرگتر بشه.نتیجه این کار افزایش Gas و درنهایت رسیدن اون به GasLimit، که عملا باعث میشه فانکشن ()distribute غیرفعال و ناکارآمد بشه.
حالا راه حل چیه؟
اول از هر چیزی حلقه زدن داده ها به طوری که توسط کاربران خارجی امکان دستکاری شدنش وجود داره، کار غلطیه! با توجه به قرارداد، به جای استفاده از روشی که در فانکشن ()distribute به کار گرفته شده به راحتی میتونیم از ساختار withdrawal استفاده کنیم تا هر شخصی که در این قرارداد هوشمند سرمایه کذار به شمار میره بتونه به صورت مستقل با فانکشن ارتباط برقرار کنه و توکن های خودش رو برداشت یا واریز کنه.
به طور کلی هر قرارداد هوشمندی که احتیاج داره از بیرون کاری انجام بشه تا به کار خودش ادامه بده، قطعا ممکنه مورد حمله قرار بگیره.بهتره که ساختار قرارداد هوشمند رو طوری بنویسیم که در ساده ترین راه، امن ترین و بهترین عملکرد رو با حداقل وابستگی داشته باشه.
استفاده از require ها و modifier ها هم تا حدودی از Attack های احتمالی جلوگیری میکنه ولی هنوز هم امنیت رو به صددرصد نمیرسونه ولی باز هم یک مرحله به ساحتار امنیتی قرارداد ما اضافه میکنه.

جمع بندی

بسیاری از توسعه دهندگان قراداد هوشمند میتوانند چیزهای زیادی یادبگیرند و درک کنند و با پیروی از بهترین روش ها در طراحی قرارداد هوشمند و کدها، از حمله های احتمالی و تله ها و مشکلات شدید جلوگیری کنند.همانطوری که ضرب المثل معروف میگه: “پیشگیری بهتر از درمانه”.
نوشتن قرارداد هوشمند به ظرافت و دقت بالایی نیاز داره.چراکه امکان بازیابی و تغییر یک قرارداد وجود نداره و برای همیشه در دنیای بلاکچین ثبت میشه.
شاید یکی از اساسی ترین اصل امنیت نرم افزار حداکثر استفاده مجدد از کدهای قابل اعتماد باشه.در دنیای رمزنگاری و قرارداد هوشمند این موضوع اهمیت بسیار زیادی داره.
پس قبل از اختراع چرخ، سعی کنید از کتابخانه هایی که توسط توسعه دهندکان دیگه بارها و بارها آزمایش شده و تایید شده استفاده کنید.
هیچ کاری به طور صددرصدی امنیت یک موضوع خاص رو تضمین نمیکنه.توی دنیای امروز که هر روز اخبار جدیدی درباره علم و فناوری شنیده میشه و تکنولوژی ساعت به ساعت درحال پیشرفته، بهترین کار اینه که حداقل درصد هارو به صد نزدیک تر کنیم تا اینکه بخواییم دنبال خود صد باشیم.

6 پسندیده

مقاله خیلی خوبی بود . تشکر از شما .

2 پسندیده

ممنون از شما ، خوشحالم که مفید بوده

2 پسندیده