Əvvəlcə, mən sizə reentrancy hücumunun nə olduğunu və bunun qarşısını necə ala biləcəyinizi sadə bir şəkildə başa salmağa çalışacağam, sonra isə kod nümunələrinə daha dərindən dalacağam ki, zəifliklərin harada olduğunu, hücumçunun kodunun nə olacağını və ən əsası, layihənizdəki bütün ağıllı müqavilələri qorumaq üçün ən son təsdiq olunmuş metodları sizə təqdim edəcəyəm.

Spoiler: Əgər artıq nonReentrant() modifikatoru haqqında eşitmisinizsə, oxumağa davam edin, çünki bir neçə sətir sonra globalNonReentrant() modifikatorunu və checks-effects-interactions naxışını kəşf etməyə hazırlaşırsınız.



Yuxarıdakı şəkildə ContractA və ContractB var. İndi bildiyiniz kimi, bir smart müqavilə başqa bir smart müqavilə ilə qarşılıqlı ola bilər, məsələn, bu vəziyyətdə ContractA ContractB-ni çağırır. Beləliklə, reentransiyanın çox əsas ideyası odur ki, ContractB ContractA-ya geri çağırış edə bilər, çünki ContractA hələ də icra olunur.

Bəs, hücumçunun bunu necə istifadə edə biləcəyini düşünək?

Yuxarıda 10 Ether olan ContractA var və görürük ki, ContractB ContractA-da 1 Ether saxlayıb. Bu halda, ContractB ContractA-dan çıxarış funksiyasını istifadə edə bilər və Ether-i özünə geri göndərə bilər, çünki onun balansı 0-dan böyükdür, sonra isə ümumi balansı 0-a dəyişəcək.



İndi gəlin ContractB-nin withdraw funksiyasını istismar etmək üçün reentransiyanı necə istifadə edə biləcəyinə baxaq və ContractA-dan bütün Etherləri oğurlayaq. Əsasən, hücumçunun iki funksiyaya ehtiyacı olacaq: attack() və fallback().

Solidity-də fallback funksiyası, adı, parametrləri və ya geri dönüş dəyərləri olmayan xarici bir funksiyadır. Hər kəs fallback funksiyasını çağıraraq çağırmaq mümkündür: Müqavilə içində olmayan bir funksiyanı çağırmaq; Tələb olunan məlumatları göndərmədən funksiyanı çağırmaq; Müqaviləyə heç bir məlumat göndermədən Ether göndərmək.

Reentransiyanın necə işlədiyi (addım-addım oxuyaq) hücumçının attack() funksiyasını çağırması ilə başlayır ki, bu da ContractA-dan withdraw() funksiyasını çağırır. Funksiyanın içində ContractB-nin balansının 0-dan böyük olub-olmadığını yoxlayacaq və əgər belədirsə, icrasını davam etdirmək üçün davam edəcək.



ContractB-nin balansı 0-dan böyük olduğuna görə, 1 Ether geri göndərir və fallback funksiyasını tetikler. Diqqət yetirin ki, bu anda ContractA-nın 9 Etheri və ContractB-nin artıq 1 Etheri var.



Sonra, fallback funksiyası icra edildikdə, yenidən ContractA-nın withdraw funksiyasını tetikler, yenidən ContractB-nin balansının 0-dan böyük olub olmadığını yoxlayır. Yenə də yuxarıdakı şəkilə baxsanız, onun balansının hələ də 1 Ether olduğunu görəcəksiniz.



Bu, yoxlamanın keçdiyini və ContractB-yə başqa bir Ether göndərdiyini bildirir ki, bu da fallback funksiyasını tetikler. Diqqət yetirin ki, burada “balance=0” sətiri heç vaxt icra edilmədiyi üçün, bu, ContractA-dan bütün Etherlər gedənə qədər davam edəcək.

___________

İndi gəlin Solidity kodu ilə reentransiyanı müəyyən edə biləcəyimiz bir smart müqaviləyə nəzər salaq.



EtherStore müqaviləsində deposit() funksiyası var ki, bu da göndərənin balansını saxlayır və yeniləyir, sonra withdrawAll() funksiyası, bu isə bütün balansları bir anda götürəcək. Xahiş edirəm, withdrawAll() -ın tətbiqinə diqqət yetirin, burada əvvəlcə require ilə balansın 0-dan böyük olduğunu yoxlayır və sonra Ether göndərir, yenə də göndərənin balansını 0-a yeniləmək üçün sona saxlayır.



Burada EtherStore müqaviləsini boşaltmaq üçün reentransiyanı istifadə edəcək hücum müqaviləsi var. Onun kodunu analiz edək:

  • Hücumçunun EtherStore ünvanını ötürərək bir nümunə yaratması üçün onun konstruktoruna daxil olacağıdır ki, bununla da onun funksiyalarını istifadə edə bilsin.

  • Burada EtherStore bu müqaviləyə Ether göndərdiyi zaman çağırılacaq fallback() funksiyasını görürük. İçərisində balans 1-ə bərabər və ya daha böyük olduğu müddətcə EtherStore-dan withdraw çağırılacaq.

  • Və attack() funksiyasının içində EtherStore-u istismar edəcək məntiqimiz var. Gördüyümüz kimi, əvvəlcə hücumu başlamaq üçün kifayət qədər ether olub-olmadığımızı təmin edəcəyik, sonra EtherStore-da 0-dan böyük bir balans əldə etmək üçün 1 ether yatıracağıq və buna görə də çıxarış etməyə başlamazdan əvvəl yoxlamaları keçəcəyik.

Yuxarıda ContractA və ContractB-nin nümunəsində kodun necə işləyəcəyini addım-addım izah etdim, indi isə bunun necə olacağını xülasə edək. İlk növbədə hücumçu attack() çağıracaq, bu içində EtherStore-dan withdrawAll() çağıracaq, bu da Etheri Attack müqaviləsinin fallback funksiyasına göndərəcək. Və burada reentransiya başlayacaq və EtherStore-un balansını boşaldacaq.

Bəs, necə edə bilərik müqavilələrimizi reentransiya hücumlarından qoruyaq?

Tam qorumaq üçün sizə üç qarşısını alma texnikası göstərəcəyəm. Mən reentransiyanın tək bir funksiyada, reentransiya funksiyalar arası və reentransiya müqavilələrarası necə qarşısını almaqdan danışacağam.



Tək bir funksiyanı qorumaq üçün birinci texnika noReentrant adlı bir modifier istifadə etməkdir.

Modifier, digər funksiyaların davranışını dəyişmək üçün istifadə olunan xüsusi bir funksiya növüdür. Modifier-lər sizə funksiyaya əlavə şərtlər və ya funksionallıq əlavə etməyə imkan verir, bütün funksiyanı yenidən yazmadan.

Burada etdiyimiz şey, funksiya icra olunarkən müqaviləni lock etməkdir. Bu yolla, tək bir funksiyaya yeniden girməyə imkan verməyəcək, çünki funksiyanın kodundan keçməli və sonra yenidən yoxlamaq üçün kilidli vəziyyət dəyişənini false-a dəyişməlidir.

___________



İkinci texnika, müqavilələrimizi funksiyalar arası reentransiyadan qorumaq üçün Checks-Effects-Interactions naxışından istifadə etməkdir. Yuxarıdakı yenilənmiş EtherStore müqaviləsində nələrin dəyişdiyini görə bilərsiniz?

Check-Effects-Interaction naxışını dərindən öyrənmək üçün https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html oxumağı tövsiyə edirəm.





Yuxarıda, soldakı görüntüdə Ether göndərdikdən sonra balansın yeniləndiyi kodun müqayisəsini görürük, yuxarıda gördüyünüz kimi, bu, heç vaxt əldə oluna bilməzdi. Sağda isə, balancer[msg.sender] = 0 (və ya effekt) tələb(bal > 0) (yoxlama) ilə düz birbaşa göndərilmədən əvvəl, göndərməyi başa çatdırmaq üçün hər şeyin yerini dəyişdirməkdir.

Bu yolla, başqa bir funksiyanın withdrawAll() -a daxil olmasına baxmayaraq, bu müqavilə hücumçudan qorunacaq, çünki balans həmişə Ether göndərmədən əvvəl yeniləcək.

Pattern created by https://twitter.com/GMX_IO

Göstərəcəyim üçüncü texnika GlobalReentrancyGuard müqaviləsini yaratmaqdır ki, bu da kontraktlararası reentransiya ilə qorumaq üçündür. Bir-biri ilə qarşılıqlı olan bir neçə müqaviləsi olan layihələr üçün bunun tətbiq olunduğunu anlamaq vacibdir.

Burada ideya birinci texnikada izah etdiyim noReentrant modifier ilə eynidir, modifier-ə daxil olur, müqaviləni kilidləmək üçün bir dəyişəni yeniləyir və kodu bitirmədiyi müddətdə onu açmır. Burada böyük fərq ondan ibarətdir ki, biz ayrıca bir müqavilədə saxlanılan bir dəyişəndən istifadə edirik ki, bu da funksiyanın daxil olub-olmadığını yoxlamaq üçün yer kimi istifadə olunur.







Burada kod olmadan və yalnız funksiya adları ilə bir nümunə yaratdım ki, bu ideyanı başa düşməyə kömək etsin, çünki mənim təcrübəmə görə, bu vəziyyəti sözlərlə yazmaqdan daha yaxşı vizuallaşdırmağa kömək edə bilər.

Burada, hücumçu ScheduledTransfer müqaviləsində funksiyanı çağıracaq ki, şərtləri yerinə yetirdikdən sonra müəyyən edilmiş Etheri AttackTransfer müqaviləsinə göndərəcək, bununla da fallback funksiyasına daxil olacaq və beləliklə ScheduledTransfer müqaviləsinin baxış bucağından “transaksiyanı ləğv edəcək” və yenə də Etheri alacaq. Və bu şəkildə ScheduledTransfer-dən bütün Etherləri boşaltmağa başlayacaq.

Bəli, yuxarıda qeyd etdiyim GlobalReentrancyGuard istifadə edərək belə bir hücum senarisi baş verməyəcək.

__________________

Twitter @TheBlockChainer Smart müqavilələr, Web3 Təhlükəsizliyi, Solidity, Smart müqavilələri audit etmək və daha çox haqqında gündəlik yenilikləri tapmaq üçün.

__________________