Ana içeriğe geç

ERC-20 ve Güvenlik Önlemleri

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

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:

  1. Remix IDE(opens in a new tab)'yi açın.
  2. Github'ı klonla simgesine tıklayın (clone github icon).
  3. Github deposunu kopyalayın https://github.com/qbzzt/20220815-erc20-safety-rails.
  4. 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:

  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. Şimdi tamamen işlevsel bir ERC-20 sözleşmemiz var. İçeri aktarılan kodu görmek için .deps > npm'yi 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'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:

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

  2. 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 virtual
3 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 virtual
Kopyala

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 olan address(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:

  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. 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:

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;
    Kopyala
  • Bir 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 unfrozen
    2 event AccountFrozen(address indexed _addr);
    3 event AccountThawed(address indexed _addr);
    Kopyala
  • 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
    Kopyala

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 } // freezeAccount
Kopyala

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 dest
4 )
5 public
6 onlyOwner
7 {
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.

Bu rehber yararlı oldu mu?