Ana içeriğe atla

Güvenlik Önlemleriyle ERC-20

erc-20
Başlangıç
Ori Pomerantz
15 Ağustos 2022
7 dakikalık okuma

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 (opens in a new tab) 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:

  1. Remix IDE (opens in a new tab)'yi açın.
  2. GitHub'ı klonla simgesine tıklayın (clone github icon).
  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ş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) (opens in a new tab) kullanacağız. Bunu başka bir tarayıcıda açın ve şu talimatları izleyin:

  1. ERC20'yi seçin.

  2. Şu ayarları girin:

    ParametreDeğer
    İsimSafetyRailsToken
    SembolSAFE
    Ön basım (Premint)1000
    ÖzelliklerYok
    Erişim KontrolüSahiplenilebilir (Ownable)
    YükseltilebilirlikYok
  3. 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.

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

  5. 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 (opens in a new tab).

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:

  1. Token'ları sözleşmenin kendi adresine göndermek. Örneğin, Optimism'in OP token'ı (opens in a new tab) iki aydan kısa bir sürede 120.000'den fazla (opens in a new tab) OP token biriktirmeyi başardı. Bu, muhtemelen insanların öylece kaybettiği önemli miktarda bir serveti temsil ediyor.

  2. 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 (opens in a new tab).

Transferleri önlemek

OpenZeppelin ERC-20 sözleşmesi, bir token transfer edilmeden önce çağrılan bir kanca (hook) olan _beforeTokenTransfer (opens in a new tab) 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) (opens in a new tab) 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:

  • to adresi, ERC-20 sözleşmesinin kendi adresi olan address(this)'e eşit olamaz.
  • to adresi 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 (opens in a new tab) 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 (opens in a new tab)'u kullanmalıyız. Solidity'den kullanabileceğimiz başka değerler de vardır (<address>.code ve <address>.codehash (opens in a new tab)), 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 (opens in a new tab) çağırırız ve ardından sıfır olmadığını kontrol etmek için GT (opens in a new tab) 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) (opens in a new tab) olabilir, böylece birden fazla kişinin bir eylem üzerinde anlaşması gerekir. Bu makalede iki yönetimsel özelliğimiz olacak:

  1. Hesapları dondurmak ve çözmek. Bu, örneğin bir hesabın ele geçirilmiş olabileceği durumlarda faydalı olabilir.

  2. 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 (opens in a new tab). Meşru ERC-20 sözleşmesi 0x4200....0042 (opens in a new tab)'dir. Oymuş gibi davranan dolandırıcılık ise 0x234....bbe (opens in a new tab)'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:

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 (opens in a new tab) değerlere bir eşleme (mapping) (opens in a new tab). 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 (opens in a new tab). 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
          onlyOwner
    

    public (opens in a new tab) 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);
      }  // freezeAccount
    

    Hesap 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 _beforeTokenTransfer iş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 (opens in a new tab) veya approve (opens in a new tab) 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ı (opens in a new tab) 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 (opens in a new tab).