Ana içeriğe geç

Güvenlik Önlemli ERC-20

erc-20
Acemi
Ori Pomerantz
15 Ağustos 2022
7 dakikalık okuma

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ı yaygın hatalardan bazılarını ve kullanıcıların bu hatalardan kaçınmasına yardımcı olan veya merkezi bir otoriteye bir miktar yetki (örneğin hesapları dondurmak için) veren ERC-20 sözleşmelerinin nasıl oluşturulacağını öğreneceksiniz.

Bu makalede OpenZeppelin ERC-20 jeton sözleşmesini (opens in a new tab) kullanacak olsak da, sözleşmenin bu makalede ayrıntılı olarak açıklanmadığını unutmayın. Bu bilgiyi burada bulabilirsiniz.

Bütün kaynak kodunu görmek isterseniz:

  1. Remix IDE (opens in a new tab)'yi açın.
  2. Github klonlama simgesine tıklayın (github klonlama simgesi).
  3. https://github.com/qbzzt/20220815-erc20-safety-rails github deposunu klonlayın.
  4. contracts > erc20-safety-rails.sol dosyasını açın.

Bir ERC-20 sözleşmesi oluşturma

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ı (opens in a new tab) kullanacağız. Başka bir tarayıcıda açın ve şu talimatları izleyin:

  1. ERC20'yi seçin.

  2. Bu ayarları girin:

    ParametreDeğer
    İsimSafetyRailsToken
    SembolSAFE
    Premint1000
    ÖzelliklerHiçbiri
    Erişim KontrolüSahiplenilebilir
    YükseltilebilirlikHiçbiri
  3. 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.

  4. Artık tamamen işlevsel bir ERC-20 sözleşmemiz var. İçe aktarılan kodu görmek için .deps > npm yolunu genişletebilirsiniz.

  5. 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'i nasıl kullanacağınızı öğrenmeniz gerekiyorsa bu öğreticiyi kullanın (opens in a new tab).

Yaygın hatalar

Hatalar

Kullanıcılar bazen jetonları yanlış adrese gönderir. Ne yapmak istediklerini bilmek için akıllarını okuyamasak da, sıkça meydana gelen ve tespit edilmesi kolay olan iki hata türü vardır:

  1. 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'in üzerinde (opens in a new tab) OP jetonu biriktirmeyi başardı. Bu, muhtemelen insanların kaybettiği önemli miktarda bir serveti temsil ediyor.

  2. Jetonları boş bir adrese, yani bir Harici Olarak Sahiplenilmiş hesaba veya bir akıllı sözleşmeye karşılık gelmeyen bir adrese göndermek. Bunun ne sıklıkla olduğuna dair istatistiklerim olmasa da, bir olay 20.000.000 jetona mal olabilirdi (opens in a new tab).

Transferleri önleme

OpenZeppelin ERC-20 sözleşmesi, bir jeton transfer edilmeden önce çağrılan _beforeTokenTransfer adında bir kanca (opens in a new tab) içerir. Varsayılan olarak bu kanca hiçbir şey yapmaz, ancak bir sorun olduğunda geri dönen kontroller gibi kendi işlevselliğimizi ona bağlayabiliriz.

Kancayı kullanmak için bu işlevi kurucudan sonra ekleyin:

1 function _beforeTokenTransfer(address from, address to, uint256 amount)
2 internal virtual
3 override(ERC20)
4 {
5 super._beforeTokenTransfer(from, to, amount);
6 }

Solidity'ye pek aşina değilseniz işlevin bazı kısımları sizin için yeni olabilir:

1 internal virtual

virtual anahtar kelimesi, tıpkı ERC20'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)

_beforeTokenTransfer'in ERC20 jeton tanımını geçersiz kıldığımızı (opens in a new tab) açıkça belirtmeliyiz. 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);

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 ERC20'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).

Gereksinimleri kodlama

Aşağıdaki gereksinimleri işleve eklemek istiyoruz:

  • to adresi, ERC-20 sözleşmesinin kendi adresi olan address(this) ile eşit olamaz.
  • to adresi boş olamaz, aşağıdakilerden birisi olmalıdır:
    • Bir Harici Olarak Sahiplenilmiş 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.
    • 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 (opens in a new tab) olarak adlandırılan bir işlem kodu vardır, ancak bu doğrudan Solidity'de mevcut değildir. Bunun için EVM assembly'si olan Yul (opens in a new tab)'u kullanmalıyız. Solidity'den kullanabileceğimiz başka değerler de var (<address>.code ve <address>.codehash (opens in a new tab)) ama bunlar daha maliyetli.

Gelin yeni kodu satır satır inceleyelim:

1 require(to != address(this), "Jetonlar sözleşme adresine gönderilemez");

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 }

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) öğesini çağırır ve sonra sıfır olmadığını doğrulamak için GT (opens in a new tab) kullanırız (negatif olamaz, çünkü işaretsiz tam sayılarla uğraşıyoruz). Sonrasında sonucu, isToContract'a yazarız.

1 require(to.balance != 0 || isToContract, "Jetonlar boş bir adrese gönderilemez");

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 potansiyelini azaltmak için bu yönetici bir çoklu imza (opens in a new tab) olabilir, böylece bir eylem üzerinde birden fazla kişinin anlaşması gerekir. Bu makale içerisinde iki yönetici özelliğinden bahsedeceğiz:

  1. Hesapları dondurmak ve çözmek. Bu, örneğin bir hesabın güvenliği ihlal edildiğinde faydalı olabilir.

  2. 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. Onun gibi davranan dolandırıcılık 0x234....bbe (opens in a new tab)'dir.

    İ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 mekanizma sunar:

Basit olması için bu makalede Ownable kullanıyoruz.

Sözleşmeleri dondurma ve çözme

Sözleşmeleri dondurmak ve çözmek, birtakım değişiklikler gerektirir:

  • Hangi adreslerin dondurulduğunu takip etmek için adreslerden boolean'lara (opens in a new tab) bir eşleme (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;
  • Bir hesap dondurulduğunda veya çözüldüğünde ilgilenen herkesi bilgilendirmek için Olaylar (opens in a new tab). Teknik olarak bu eylemler için olaylar gerekli değildir, ancak zincir dışı kodların bu olayları dinlemesine ve ne olduğunu bilmesine yardımcı olur. Akıllı bir sözleşmenin başkasıyla alakalı olabilecek bir şey olduğunda bunları yayımlaması iyi bir davranış olarak kabul edilir.

    Olaylar dizine eklendiğinden, bir hesabın dondurulduğu veya çözüldüğü tüm zamanlar aranabilir.

    1 // Hesaplar dondurulduğunda veya çözüldüğünde
    2 event AccountFrozen(address indexed _addr);
    3 event AccountThawed(address indexed _addr);
  • Hesapları 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 public
    3 onlyOwner

    public (opens in a new tab) olarak işaretlenen işlevler, diğer akıllı sözleşmelerden veya doğrudan bir işlemle çağrılabilir.

    1 {
    2 require(!frozenAccounts[addr], "Hesap zaten dondurulmuş");
    3 frozenAccounts[addr] = true;
    4 emit AccountFrozen(addr);
    5 } // freezeAccount

    Hesap zaten dondurulmuşsa, geri alın. Aksi takdirde, dondurun ve bir olay emit edin.

  • Dondurulmuş bir hesaptan para taşınmasını önlemek için _beforeTokenTransfer'i değiştirin. Donmuş hesaba halen para aktarılabileceğini unutmayın.

    1 require(!frozenAccounts[from], "Hesap dondurulmuş");

Varlık temizleme

Bu sözleşme tarafından 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 dest
4 )
5 public
6 onlyOwner
7 {
8 IERC20 token = IERC20(erc20);

Bu, adresi aldığımızda bir sözleşme için nesne oluşturma söz dizimidir. Bunu yapabiliriz çünkü kaynak kodun bir parçası olarak ERC20 jetonlarının tanımına sahibiz (bkz. satır 4) ve bu dosya, bir OpenZeppelin ERC-20 sözleşmesinin arayüzü olan IERC20 tanımını (opens in a new tab) içerir.

1 uint balance = token.balanceOf(address(this));
2 token.transfer(dest, balance);
3 }

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ç

Bu mükemmel bir çözüm değildir; "kullanıcı hata yaptı" sorununun mükemmel bir çözümü 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.

Çalışmalarımdan daha fazlası için buraya bakın (opens in a new tab).

Sayfanın son güncellenmesi: 4 Eylül 2025

Bu rehber yararlı oldu mu?