ERC-20 ve Güvenlik Önlemleri
Giriş
Ethereum ile ilgili en güzel şeylerden biri, işlemlerinizi değiştirebilecek veya geri alabilecek merkezi herhangi bir otoritenin olmamasıdır. Ethereum ile ilgili en büyük sorunlardan biri, kullanıcı hatalarını veya yasa dışı işlemleri geri alma yetkisine sahip merkezi bir otoritenin olmamasıdır. Bu makalede, kullanıcıların ERC-20 jetonlarıyla yaptıkları bazı yaygın hataların yanı sıra; kullanıcıların bu hatalardan kaçınmasına yardımcı olan veya merkezi bir otoriteye bir miktar yetki veren (örneğin hesapları dondurmak için) ERC-20 sözleşmelerinin nasıl oluşturulacağını öğreneceksiniz.
Open Zeppelin ERC-20 jeton sözleşmesini(opens in a new tab) hala kullanıyor olsak da bu makalenin bunu ayrıntılı olarak açıklamadığını söylemiş olalım. Bu bilgiyi burada bulabilirsiniz.
Bütün kaynak kodunu görmek isterseniz:
- Remix IDE(opens in a new tab)'yi açın.
- Github'ı klonla simgesine tıklayın ().
- Github deposunu kopyalayın
https://github.com/qbzzt/20220815-erc20-safety-rails
. - Sözleşmeler > erc20-safety-rails.sol öğesini açın.
ERC-20 sözleşmesi oluşturma
Güvenlik önlemi işlevini eklemeden önce bize bir ERC-20 sözleşmesi lazım. Bu makalede OpenZeppelin'in Sözleşme Sihirbazı'nı(opens in a new tab) kullanacağız. Başka bir sekmede açın ve şu yönergeleri izleyin:
ERC20'yi seçin.
Bu ayarları girin:
Parametre Değer İsim SafetyRailsToken Sembol SAFE Premint 1000 Özellikler Hiçbiri Erişim Kontrolü Sahiplenilebilir Yükseltilebilirlik Hiçbiri Yukarı kaydırın ve farklı bir ortam kullanmak için Remix'te Aç'a (Remix için) veya İndir'e tıklayın. Remix kullandığınızı varsayacağım, başka bir şey kullanıyorsanız sadece uygun değişiklikleri yapın.
Şimdi tamamen işlevsel bir ERC-20 sözleşmemiz var. İçeri aktarılan kodu görmek için
.deps
>npm
'yi genişletebilirsiniz.Bir ERC-20 sözleşmesi olarak işlev gördüğünü anlamak için sözleşmeyi derleyin, dağıtın ve sözleşmeyle oynayın. Remix'in nasıl kullanıldığını öğrenmek istiyorsanız bu rehberi kullanın(opens in a new tab).
Yaygın hatalar
Hatalar
Kullanıcılar bazen jetonları yanlış adrese gönderir. Ne yapmak istediklerini anlamak için zihinlerini okuyamasak da sık sık meydana gelen ve tespit edilmesi kolay olan iki hata türü vardır:
Jetonları sözleşmenin kendi adresine göndermek. Örneğin, Optimism'in OP jetonu(opens in a new tab) iki aydan kısa bir sürede 120.000'den(opens in a new tab) fazla OP jetonu biriktirmeyi başardı. Bu, muhtemelen insanların kaybettiği önemli miktarda bir serveti temsil ediyor.
Jetonları harici bir dışarıdan sahip olunan hesaba veya akıllı sözleşmeye karşılık gelmeyen boş bir adrese göndermek. Bunun ne sıklıkla gerçekleştiğine dair istatistiklere sahip olmasam da tek bir olay 20.000.000 jetona mal olabilir.(opens in a new tab).
Transferleri önleme
OpenZeppelin ERC-20 sözleşmesi, jeton aktarılmadan önce çağrılan bir _beforeTokenTransfer
kancası içerir. Bu kanca çalışırken varsayılan olarak hiçbir şey yapmaz ancak örneğin bir sorun olduğunda geri dönen kontroller gibi kendi işlevselliğimizi içerisine ekleyebiliriz.
Kancayı kullanmak için oluşturucudan sonra şu işlevi ekleyin:
1 function _beforeTokenTransfer(address from, address to, uint256 amount)2 internal virtual3 override(ERC20)4 {5 super._beforeTokenTransfer(from, to, amount);6 }Kopyala
Solidity'ye pek aşina değilseniz işlevin bazı kısımları sizin için yeni olabilir:
1 internal virtualKopyala
virtual
anahtar sözcüğü tıpkı bizim ERC-20
'den işlevsellik devraldığımız ve bu işlevi geçersiz kıldığımız gibi diğer sözleşmelerin de bizden miras alıp bu işlevi geçersiz kılabileceği anlamına gelir.
1 override(ERC20)Kopyala
_beforeTokenTransfer
'in ERC-20 jeton tanımını geçersiz kıldığımızın(opens in a new tab) altını çizmeliyiz. Genel olarak net tanımlar örtülü tanımlara göre çok daha iyidir; bir şey gözünüzün tam önündeyse ne yaptığınızı unutmazsınız. Hangi üst sınıfa ait _beforeTokenTransfer
'i geçersiz kıldığımızı belirtmemizin sebebi de budur.
1 super._beforeTokenTransfer(from, to, amount);Kopyala
Bu satır, sözleşmenin veya ona sahip olan devraldığımız sözleşmelerin _beforeTokenTransfer
işlevini çağırır. Bu durumda, bahsettiğimiz sadece ERC-20
'dir, Ownable
bu kancaya sahip değildir. Halihazırda ERC20._beforeTokenTransfer
hiçbir şey yapmasa da, gelecekte işlevsellik eklenmesi durumunda onu çağırabiliriz (ve sonra, dağıtımdan sonra sözleşmeler değişmediği için sözleşmeyi yeniden dağıtmaya karar veririz).
Gereksinimlerin kodlanması
Aşağıdaki gereksinimleri işleve eklemek istiyoruz:
to
adresi, ERC-20 sözleşmesinin kendi adresi olanaddress(this)
ile eşit olamaz.to
adresi boş olamaz, aşağıdakilerden birisi olmalıdır:- Dışarıdan sahip olunan hesap (EOA). Bir adresin EOA olup olmadığını doğrudan kontrol edemeyiz ancak bir adresin ETH bakiyesini kontrol edebiliriz. EOA'ların artık kullanılmasalar bile neredeyse her zaman bir bakiyesi vardır; onları son wei'ye kadar temizlemek zordur.
- Akıllı sözleşme. Bir adresin akıllı sözleşme olup olmadığını test etmek biraz daha zordur. Harici kod uzunluğunu kontrol eden,
EXTCODESIZE
(opens in a new tab) isimli bir işlem kodu vardır ancak doğrudan Solidity'de mevcut değildir. Bunun için EVM derlemesi olan Yul(opens in a new tab)'u kullanmamız gerekir. Solidity'den kullanabileceğimiz (<address>.code
ve<address>.codehash
(opens in a new tab)) gibi başka değerler de vardır ancak maliyetleri daha yüksektir.
Gelin yeni kodu satır satır inceleyelim:
1 require(to != address(this), "Can't send tokens to the contract address");Kopyala
to
ile this(address)
'in aynı şey olmadığını kontrol edin; bu, ilk gerekliliktir.
1 bool isToContract;2 assembly {3 isToContract := gt(extcodesize(to), 0)4 }Kopyala
Biz bir adresin sözleşme olup olmadığını bu şekilde kontrol ederiz. Yul'dan çıktıyı doğrudan alamayız, bunun yerine sonucu tutabilecek bir değişken tanımlarız (bu durumda isToContract
). Yul'un çalışma şekli, her işlem kodunun bir işlev olarak kabul edilmesidir. Öncelikle sözleşme boyutunu almak için EXTCODESIZE
(opens in a new tab)'ı çağırır ve sonra sıfır olmadığını doğrulamak için GT
(opens in a new tab)'yi kullanırız (negatif olamaz, çünkü işaretsiz tam sayılarla uğraşıyoruz). Sonrasında sonucu, isToContrac
'a yazarız.
1 require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");Kopyala
Son olarak sırada, boş adresler için gerçek kontrolümüz var.
Yönetici erişimi
Hataları geri alabilen bir yöneticiye sahip olmak bazen faydalı olabilir. Kötüye kullanım olasılığını azaltmak için bu yönetici bir çoklu imza(opens in a new tab) olabilir, böylece birden fazla kişinin bir eylem üzerinde anlaşması gerekir. Bu makale içerisinde iki yönetici özelliğinden bahsedeceğiz:
Hesapları dondurmak ve çözmek. Bu, örneğin bir hesabın güvenliği ihlal edildiğinde faydalı olabilir.
Varlık temizlemesi.
Bazen dolandırıcılar, meşruiyet kazanmak için gerçek jetonun sözleşmesine hileli jetonlar gönderir. Örneğin, buraya bakın(opens in a new tab). Meşru ERC-20 sözleşmesi 0x4200....0042(opens in a new tab)'dir. 0x234....bbe(opens in a new tab) imiş gibi davranan bir dolandırıcılıktır.
İnsanların sözleşmemize yanlışlıkla başka geçerli jetonlar göndermesi ise onları oradan çıkarmanın bir yolunu bulmamız için başka bir nedendir.
OpenZeppelin, yönetici erişimini etkinleştirmek için iki çeşit mekanizma sunar:
Ownable
(opens in a new tab) sözleşmelerinin sadece bir sahibi vardır.OnlyOwner
özelliğine(opens in a new tab) sahip işlevler yalnızca o sahip tarafından çağrılabilir. Sahipler bu sahipliği bir başkasına devredebilir ya da tamamen sahiplikten feragat edebilir. Tüm diğer hesapların hakları ise genelde aynıdır.AccessControl
(opens in a new tab) sözleşmelerinde rol tabanlı erişim kontrolü (RBAC) bulunur(opens in a new tab).
Basit olması için biz bu makalede Ownable
'ı kullanacağız.
Sözleşmeleri dondurma ve çözme
Sözleşmeleri dondurmak ve çözmek, birtakım değişiklikler gerektirir:
Hangi adreslerin dondurulduğunu takip etmeye yarayan adresler ile boole değerleri(opens in a new tab) eşlemesi(opens in a new tab). Tüm değerler başlangıçta sıfırdır ve bu boole değerleri için yanlış olarak yorumlanır. Hesaplar varsayılan olarak dondurulmuş halde gelmediği için istediğimiz budur.
1 mapping(address => bool) public frozenAccounts;KopyalaBir hesap dondurulduğunda veya çözüldüğünde ilgilenen herkesi bilgilendirmek için olay akışı(opens in a new tab). Teknik olarak yaklaşacak olursak olay akışı bu eylemler için gerekli değildir ancak zincir dışı kodların olayı izlemesi ve olan biteni anlamasına yardımcı olurlar. Akıllı bir sözleşmenin başkasıyla alakalı olabilecek bir şey olduğunda bunları yayımlaması iyi bir davranış olarak kabul edilir.
Olay akışı dizine eklenir, böylece bir hesabın dondurulduğu veya çözüldüğü tüm zamanları aramak mümkün olur.
1 // When accounts are frozen or unfrozen2 event AccountFrozen(address indexed _addr);3 event AccountThawed(address indexed _addr);KopyalaHesapları dondurmaya ve çözmeye yarayan işlevler. Bu iki fonksiyon neredeyse aynıdır; bu yüzden, dondurma fonksiyonu üzerinden ilerleyeceğiz.
1 function freezeAccount(address addr)2 public3 onlyOwnerKopyala
public
(opens in a new tab) olarak işaretlenmiş işlevler, diğer akıllı sözleşmelerden veya doğrudan bir işlemle çağrılabilir.
1 {2 require(!frozenAccounts[addr], "Account already frozen");3 frozenAccounts[addr] = true;4 emit AccountFrozen(addr);5 } // freezeAccountKopyala
Hesap önceden dondurulmuşsa, eski haline döndürün. Aksi takdirde, dondurun ve bir olay emit
edin.
Paranın donmuş bir hesaptan taşınmasını önlemek için
_beforeTokenTransfer
'i değiştirin. Donmuş hesaba halen para aktarılabileceğini unutmayın.1 require(!frozenAccounts[from], "The account is frozen");Kopyala
Varlık temizlemesi
Bu sözleşmede tutulan ERC-20 jetonlarını serbest bırakmak için ait oldukları jeton sözleşmesinde transfer
(opens in a new tab) veya approve
(opens in a new tab) işlevlerinden birini çağırmamız gerekir. Bu durumda ödenekler için gaz harcamaya gerek yoktur, doğrudan transfer edebiliriz.
1 function cleanupERC20(2 address erc20,3 address dest4 )5 public6 onlyOwner7 {8 IERC20 token = IERC20(erc20);Kopyala
Bu, adresi aldığımızda bir sözleşme için nesne oluşturma söz dizimidir. Kaynak kodun bir parçası olarak ERC-20 jetonlarının tanımına sahip olduğumuzdan (bkz. Satır 4) ve bu dosya bir OpenZeppelin ERC-20 sözleşmesinin arayüzü olan IERC20(opens in a new tab) tanımını içerdiğinden bunu yapabiliriz.
1 uint balance = token.balanceOf(address(this));2 token.transfer(dest, balance);3 }Kopyala
Bu bir temizleme işlevidir, bu yüzden muhtemelen herhangi bir jeton bırakmak istemeyiz. Bakiyeyi kullanıcıdan manuel olarak almak yerine süreci otomatikleştirebiliriz.
Sonuç
Anlattığımız süreç mükemmel bir çözüm değildir, zaten "kullanıcı bir hata yaptı" sorunları için mükemmel bir çözüm de yoktur. Ancak bu tür kontrollerin kullanılması en azından bazı hataları önleyebilir. Hesapları dondurma yeteneği, tehlikeli olmakla birlikte belirli hacker'ların çaldığı fonları reddederek hacker'ların yarattığı zararı sınırlandırmak için kullanılabilir.
Son düzenleme: @omahs(opens in a new tab), 17 Şubat 2024