Güvenlik Önlemleriyle ERC-20
Giriş
Ethereum'un harika yanlarından biri, işlemlerinizi değiştirebilecek veya geri alabilecek merkezi bir otoritenin olmamasıdır. Ethereum ile ilgili en büyük sorunlardan biri de 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 token'ları ile 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 güç (örneğin hesapları dondurmak için) veren ERC-20 sözleşmelerinin nasıl oluşturulacağını öğreneceksiniz.
Bu makalede OpenZeppelin ERC-20 token sözleşmesini (yeni sekmede açılır) kullanacak olsak da, bunun çok ayrıntılı bir şekilde açıklanmadığını unutmayın. Bu bilgiyi burada bulabilirsiniz.
Tam kaynak kodunu görmek isterseniz:
- Remix IDE (yeni sekmede açılır)'yi açın.
- GitHub'ı klonla simgesine tıklayın (
). https://github.com/qbzzt/20220815-erc20-safety-railsGitHub deposunu klonlayın.- contracts > erc20-safety-rails.sol dosyasını açın.
Bir ERC-20 sözleşmesi oluşturmak
Güvenlik önlemi işlevselliğini eklemeden önce bir ERC-20 sözleşmesine ihtiyacımız var. Bu makalede OpenZeppelin Sözleşme Sihirbazı'nı (Contracts Wizard) (yeni sekmede açılır) kullanacağız. Bunu başka bir tarayıcıda açın ve şu talimatları izleyin:
-
ERC20'yi seçin.
-
Şu ayarları girin:
Parametre Değer İsim SafetyRailsToken Sembol SAFE Ön basım (Premint) 1000 Özellikler Yok Erişim Kontrolü Sahiplenilebilir (Ownable) Yükseltilebilirlik Yok -
Yukarı kaydırın ve (Remix için) Open in Remix'e veya farklı bir ortam kullanmak için Download'a tıklayın. Remix kullandığınızı varsayacağım, başka bir şey kullanıyorsanız uygun değişiklikleri yapmanız yeterlidir.
-
Artık tamamen işlevsel bir ERC-20 sözleşmemiz var. İçe aktarılan kodu görmek için
.deps>npmbölümünü genişletebilirsiniz. -
Bir ERC-20 sözleşmesi olarak çalıştığını görmek için sözleşmeyi derleyin, dağıtın ve onunla denemeler yapın. Remix'i nasıl kullanacağınızı öğrenmeniz gerekiyorsa, bu öğreticiyi kullanın (yeni sekmede açılır).
Yaygın hatalar
Hatalar
Kullanıcılar bazen token'ları yanlış adrese gönderirler. Ne yapmak istediklerini bilmek için zihinlerini okuyamasak da, çok sık meydana gelen ve tespit edilmesi kolay iki hata türü vardır:
-
Token'ları sözleşmenin kendi adresine göndermek. Örneğin, Optimism'in OP token'ı (yeni sekmede açılır) iki aydan kısa bir sürede 120.000'den fazla (yeni sekmede açılır) OP token biriktirmeyi başardı. Bu, muhtemelen insanların öylece kaybettiği önemli miktarda bir serveti temsil ediyor.
-
Token'ları boş bir adrese, yani dışarıdan sahipli bir hesaba (EOA) veya bir akıllı sözleşmeye karşılık gelmeyen bir adrese göndermek. Bunun ne sıklıkla gerçekleştiğine dair istatistiklerim olmasa da, tek bir olay 20.000.000 token'a mal olabilirdi (yeni sekmede açılır).
Transferleri önlemek
OpenZeppelin ERC-20 sözleşmesi, bir token transfer edilmeden önce çağrılan bir kanca (hook) olan _beforeTokenTransfer (yeni sekmede açılır) içerir. Varsayılan olarak bu kanca hiçbir şey yapmaz, ancak bir sorun olduğunda işlemi geri alan kontroller gibi kendi işlevselliğimizi ona ekleyebiliriz.
Kancayı kullanmak için, bu işlevi kurucudan sonra ekleyin:
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal virtual
override(ERC20)
{
super._beforeTokenTransfer(from, to, amount);
}
Solidity'ye çok aşina değilseniz bu işlevin bazı kısımları yeni olabilir:
internal virtual
virtual anahtar kelimesi, tıpkı ERC20'den işlevselliği devraldığımız ve bu işlevi geçersiz kıldığımız (override) gibi, diğer sözleşmelerin de bizden devralabileceği ve bu işlevi geçersiz kılabileceği anlamına gelir.
override(ERC20)
_beforeTokenTransfer'un ERC20 token tanımını geçersiz kıldığımızı (override) (yeni sekmede açılır) açıkça belirtmeliyiz. Genel olarak, açık tanımlar güvenlik açısından örtük olanlardan çok daha iyidir; eğer tam önünüzdeyse bir şey yaptığınızı unutamazsınız. Hangi üst sınıfın _beforeTokenTransfer işlevini geçersiz kıldığımızı belirtmemizin nedeni de budur.
super._beforeTokenTransfer(from, to, amount);
Bu satır, devraldığımız ve bu işleve sahip olan sözleşmenin veya sözleşmelerin _beforeTokenTransfer işlevini çağırır. Bu durumda, bu sadece ERC20'dir, Ownable bu kancaya sahip değildir. Şu anda ERC20._beforeTokenTransfer hiçbir şey yapmasa da, gelecekte işlevsellik eklenmesi ihtimaline karşı onu çağırıyoruz (ve daha sonra sözleşmeyi yeniden dağıtmaya karar veririz, çünkü sözleşmeler dağıtımdan sonra değişmez).
Gereksinimleri kodlamak
İşleve şu gereksinimleri eklemek istiyoruz:
toadresi, ERC-20 sözleşmesinin kendi adresi olanaddress(this)'e eşit olamaz.toadresi boş olamaz, şunlardan biri olmalıdır:- Dışarıdan sahipli bir hesap (EOA). Bir adresin doğrudan bir EOA olup olmadığını kontrol edemeyiz, ancak bir adresin ETH bakiyesini kontrol edebiliriz. EOA'lar artık kullanılmasalar bile neredeyse her zaman bir bakiyeye sahiptir; onları son Wei'ye kadar temizlemek zordur.
- Bir 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(yeni sekmede açılır) adında bir işlem kodu vardır, ancak bu doğrudan Solidity'de mevcut değildir. Bunun için EVM assembly'si olan Yul (yeni sekmede açılır)'u kullanmalıyız. Solidity'den kullanabileceğimiz başka değerler de vardır (<address>.codeve<address>.codehash(yeni sekmede açılır)), ancak bunların maliyeti daha yüksektir.
Yeni kodun üzerinden satır satır geçelim:
require(to != address(this), "Can't send tokens to the contract address");
Bu ilk gereksinimdir, to ve this(address)'in aynı şey olmadığını kontrol edin.
bool isToContract;
assembly {
isToContract := gt(extcodesize(to), 0)
}
Bir adresin sözleşme olup olmadığını bu şekilde kontrol ederiz. Yul'dan doğrudan çıktı alamayız, bu yüzden bunun yerine sonucu tutacak 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. Bu yüzden önce sözleşme boyutunu almak için EXTCODESIZE (yeni sekmede açılır) çağırırız ve ardından sıfır olmadığını kontrol etmek için GT (yeni sekmede açılır) kullanırız (işaretsiz tam sayılarla uğraşıyoruz, bu yüzden elbette negatif olamaz). Daha sonra sonucu isToContract değişkenine yazarız.
require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");
Ve son olarak, boş adresler için asıl kontrolümüz var.
Yönetimsel erişim
Bazen hataları geri alabilen bir yöneticiye sahip olmak faydalıdır. Kötüye kullanım potansiyelini azaltmak için, bu yönetici bir çoklu imza (multisig) (yeni sekmede açılır) olabilir, böylece birden fazla kişinin bir eylem üzerinde anlaşması gerekir. Bu makalede iki yönetimsel özelliğimiz olacak:
-
Hesapları dondurmak ve çözmek. Bu, örneğin bir hesabın ele geçirilmiş olabileceği durumlarda faydalı olabilir.
-
Varlık temizliği.
Bazen dolandırıcılar meşruiyet kazanmak için gerçek token'ın sözleşmesine sahte token'lar gönderirler. Örneğin, buraya bakın (yeni sekmede açılır). Meşru ERC-20 sözleşmesi 0x4200....0042 (yeni sekmede açılır)'dir. Oymuş gibi davranan dolandırıcılık ise 0x234....bbe (yeni sekmede açılır)'dir.
İnsanların yanlışlıkla sözleşmemize meşru ERC-20 token'ları göndermesi de mümkündür, bu da onları dışarı çıkarmanın bir yolunu istemek için başka bir nedendir.
OpenZeppelin, yönetimsel erişimi sağlamak için iki mekanizma sunar:
Ownable(yeni sekmede açılır) sözleşmelerinin tek bir sahibi vardır.onlyOwnerdeğiştiricisine (modifier) (yeni sekmede açılır) sahip işlevler yalnızca o sahip tarafından çağrılabilir. Sahipler, sahipliği başkasına devredebilir veya tamamen feragat edebilirler. Diğer tüm hesapların hakları tipik olarak aynıdır.AccessControl(yeni sekmede açılır) sözleşmeleri role dayalı erişim kontrolüne (RBAC) (yeni sekmede açılır) sahiptir.
Basitlik adına, bu makalede Ownable kullanıyoruz.
Sözleşmeleri dondurmak ve çözmek
Sözleşmeleri dondurmak ve çözmek birkaç değişiklik gerektirir:
-
Hangi adreslerin dondurulduğunu takip etmek için adreslerden boolean (yeni sekmede açılır) değerlere bir eşleme (mapping) (yeni sekmede açılır). Tüm değerler başlangıçta sıfırdır, bu da boolean değerler için yanlış (false) olarak yorumlanır. İstediğimiz de budur çünkü varsayılan olarak hesaplar dondurulmamıştır.
mapping(address => bool) public frozenAccounts; -
Bir hesap dondurulduğunda veya çözüldüğünde ilgilenen herkesi bilgilendirmek için olaylar (yeni sekmede açılır). Teknik olarak konuşursak, bu eylemler için olaylar gerekli değildir, ancak zincir dışı kodun bu olayları dinleyebilmesine ve ne olduğunu bilmesine yardımcı olur. Başkasıyla ilgili olabilecek bir şey olduğunda bir akıllı sözleşmenin bunları yayması (emit) iyi bir davranış olarak kabul edilir.
Olaylar indekslenmiştir, böylece bir hesabın dondurulduğu veya çözüldüğü tüm zamanları aramak mümkün olacaktır.
// Hesaplar dondurulduğunda veya çözüldüğünde event AccountFrozen(address indexed _addr); event AccountThawed(address indexed _addr); -
Hesapları dondurmak ve çözmek için işlevler. Bu iki işlev neredeyse aynıdır, bu yüzden sadece dondurma işlevinin üzerinden geçeceğiz.
function freezeAccount(address addr) public onlyOwnerpublic(yeni sekmede açılır) olarak işaretlenmiş işlevler, diğer akıllı sözleşmelerden veya doğrudan bir işlem tarafından çağrılabilir.{ require(!frozenAccounts[addr], "Account already frozen"); frozenAccounts[addr] = true; emit AccountFrozen(addr); } // freezeAccountHesap zaten dondurulmuşsa, işlemi geri alın. Aksi takdirde, dondurun ve bir olay
emit(yayınlayın). -
Dondurulmuş bir hesaptan para taşınmasını önlemek için
_beforeTokenTransferişlevini değiştirin. Dondurulmuş hesaba hala para transfer edilebileceğini unutmayın.require(!frozenAccounts[from], "The account is frozen");
Varlık temizliği
Bu sözleşme tarafından tutulan ERC-20 token'larını serbest bırakmak için, ait oldukları token sözleşmesinde transfer (yeni sekmede açılır) veya approve (yeni sekmede açılır) işlevini çağırmamız gerekir. Bu durumda izinler (allowances) için Gaz israf etmenin bir anlamı yoktur, doğrudan transfer edebiliriz.
function cleanupERC20(
address erc20,
address dest
)
public
onlyOwner
{
IERC20 token = IERC20(erc20);
Bu, adresi aldığımızda bir sözleşme için nesne oluşturmanın sözdizimidir. Bunu yapabiliriz çünkü kaynak kodunun bir parçası olarak ERC20 token'ları için tanıma sahibiz (bkz. satır 4) ve bu dosya, bir OpenZeppelin ERC-20 sözleşmesinin arayüzü olan IERC20 tanımını (yeni sekmede açılır) içerir.
uint balance = token.balanceOf(address(this));
token.transfer(dest, balance);
}
Bu bir temizleme işlevidir, bu yüzden muhtemelen hiçbir token bırakmak istemiyoruz. Bakiyeyi kullanıcıdan manuel olarak almak yerine, süreci otomatikleştirebiliriz.
Sonuç
Bu mükemmel bir çözüm değildir - "kullanıcı hata yaptı" sorunu için mükemmel bir çözüm yoktur. Ancak, bu tür kontrolleri kullanmak en azından bazı hataları önleyebilir. Hesapları dondurma yeteneği, tehlikeli olsa da, bilgisayar korsanının çalınan fonlara erişimini reddederek belirli saldırıların (hack) hasarını sınırlamak için kullanılabilir.
Çalışmalarımın daha fazlası için buraya bakın (yeni sekmede açılır).