Merkle Tree İle Whitelist Oluşturma
Merkle tree kullanarak gerek NFT koleksiyonlarında, gerekse farklı amaçlar için nasıl whitelist oluşturabiliriz, sınırsız sayıda adresi nasıl 64 karakterli ufacık bir veride saklayabiliriz?
Merhabalar. Bu yazımızda whitelist yada bir diğer adıyla allowlist oluşturmayı ele alacağız. Basitçe tarif etmek gerekirse herhangi bir amaç için belirli bir dizi adresi barındıran listeye whitelist diyoruz.
En yaygın kullanımı NFT koleksiyonlarının belirli kullanıcılara mint ayrıcalığı tanımasıyla oluyor fakat farklı kullanım şekilleri de mevcuttur. Sizin yazdığınız kontrattaki ihtiyacınıza göre şekillenebilir. Yazıda bulunan tüm kodlara buradaki github linkinden erişebilirsiniz.
Merkle Tree ile Whitelist
Merkle tree ile whitelist oluşturmadan önce Merkle Tree Nedir? adlı yazımızı okumanızı öneriyorum. Merkle tree’nin nasıl çalıştığını öğrendikten sonra bu güzel teknolojiyi kontratlarımızda kullanmaya başlayabiliriz.
Merkle tree ile sınırsız sayıda adresi kök (root) adını verdiğimiz 64 karakterli küçük bir hash verisinde saklayabiliyoruz. Herhangi bir adresin bu listede yer alıp almadığını kontrol etmemiz için ise her adrese özel üretilen kanıtlara (proof) ihtiyaç duyuyoruz.
Kanıtlar her adrese özel üretildiği kanıt üretme işini son kullanıcı için kendi websitemizde biz sağlıyoruz. Son kullanıcı cüzdanını siteye bağlayıp mint tuşuna bastıktan sonra biz bu cüzdan adresinin listede olup olmadığını kontrol edip, eğer listedeyse bu adrese özel olan kanıtı, adresle birlikte kontrata gönderiyoruz. Kontrata yüklediğimiz kök ile girdi olarak verdiğimiz adres ve kanıt örtüşürse mint işlemine devam ettiriyoruz. Şimdi kodlara geçelim.
Kontrat
Kodları bir bütün olarak buradaki Github linkinden inceleyebilirsiniz. Ben yazıda sadece işimize yarayan kısımları dahil edeceğim ve aşama aşama ilerleyeceğim.
Öncelikle merkle root’umuzu kontratta bytes32 türünde tutuyoruz. Root’u ister kontratı oluştururken en başta, istersek de bir fonksiyon ile daha sonradan ekleyebiliriz. Root’un public olması önemli. Whitelist’inizi off-chain olarak herkese açık paylaşmanız ve bu liste ile oluşturulan kökün kontratta herkese açık olarak gözükmesi blockchain’in trustless felsefesine uygun bir hareket olacaktır.
bytes32 public merkleRoot = 0x235a431d30b7cc19b656d1ef14a6c5a257aab377a5854800c2d5446a1d3beb33
Listemize yeni bir adres eklediğimizde, çıkarttığımızda veya herhangi bir adresi güncellediğimizde bize yepyeni bir merkle root üreteceği için, bu güncellenmiş kökü kontrata yükleyebilmek için bir fonksiyon da eklememiz gerekiyor.
function setMerkleRoot(bytes32 _newRoot) external onlyOwner {
merkleRoot = _newRoot;
}
Root ekleme fonksiyonumuz sadece dışarıdan çağıralacağı için external ve sadece kontrat sahibinin yetkisinde bir eylem olduğu için onlyOwner modifier’ımız var. Kökümüzü yükledik ve güncelleme fonksiyonumuz da var. Tek ihtiyacımız olan şey kontratla iletişime geçen adresin ve onun yanında getirdiği kanıtın kök ile uyumunu kontrol etmek.
function merkleCheck(bytes32[] calldata _merkleProof) private view returns (bool) {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
return MerkleProof.verify(_merkleProof, merkleRoot, leaf);
}
merkleCheck adlı fonksiyonumuz proof’u parametre olarak alıyor, kontratla etkileşime geçen adresten leaf üretiyor ve bununla birlikte kontata yüklenmiş olan kökü de kullanarak MerkleProof.verify fonksiyonuyla bu adresin listede olup olmadığını kontrol ediyor. MerkleProof.verify fonksiyonunun sonucunu (boolean) geri döndürüyoruz. Verify fonksiyonunu kullanabilmek için MerkleProof.sol kütüphanesini de kontrata eklemiş olmamız gerekiyor. Bunu da github’daki kodlarda en üstte görebileceksiniz.
Merkle tree ile whitelist oluşturmak kontratta buraya kadardı. Artık kontrata oluşturduğumuz merkle ağacının kökünü yükleyebiliyor ve kontratla iletişime geçen adresin yanında getirdiği kanıtla birlikte bizim oluşturduğumuz kökün içerisinde olup olmadığını kontrol edebiliyoruz. Buradan sonra bununla ne yapmak istediğimiz tamamen bize kalmış. NFT mint mi yaptıracağız yoksa token claim mi ettireceğiz, tamamen bize kalmış.
Mesela listede olanlara free NFT mintletiyoruz diyelim. Örnek fonksiyonu şu şekilde olacaktır:
function whitelistMint(bytes32[] calldata _merkleProof) public {
require(merkleCheck(_merkleProof), "You are not in the whitelist!");
require(!claimed[_msgSender(), "You have already minted!");
claimed[_msgSender()] = true;
safeMint(_msgSender());
}
İşte bu kadar! Public olarak herkesin erişimine açık fonksiyonumuz parametre olarak proof alıyor. Bu kanıtı yukarıda yazdığımız merkleCheck fonksiyonuna gönderiyor. Eğer kontratla iletişime geçen adres (_msgSender()
) listedeyse true, değilse false dönüyor. False dönmesi haline require fonksiyonumuz yapılan işlemi geri çevirip “Whitelist’de değilsin” hata mesajını göndereriyor. Eğer true dönerse, require fonksiyonumuz işlemin devam etmesine izin veriyor ve bir alt satırda biz de kontratla iletişime geçen bu adrese safeMint()
fonksiyonu ile 1 adet NFT mintliyoruz.
Access Modifier
Yukarıda onlyOwner diye bir access modifier kullandık. Access modifier’lar hangi fonksiyona kimin erişebileceğini berlileyen yapılar. Örnek olarak sadece kontrat sahibinin erişmesi için onlyOwner modifier’ı aşağıdaki şekildedir:
modifier onlyOwner() {
require(owner() == _msgSender(), "Ownable: caller is not the owner");
_;
}
Gördüğünüz üzere require fonksiyonu ile kontrala iletişime geçen kişinin kontratın sahibi olması gerekliliğini şart koşuyoruz. Bu tek satırlık require fonksiyonunu hangi fonksiyonumuza eklersek oraya bu erişim kısıtlamasını getiriyoruz. Bunu bir modifier içerisinde koyarak erişim kısıtlaması getirmek istediğimiz fonksiyonlara uzun uzun bir require satırı yazmak yerine fonksiyonun sonunda yukarıdaki örnekte gördüğünüz gibi onlyOwner eki getirerek, erişim kısıtlamasını sağlıyoruz. Bu hem yazdığımız kodun okunabilirliğini arttırıyor hem de birden fazla kullanım yaptığımızda daha az kod yazmamızı sağlıyor.
Web
Şimdi gelelim işin son kullanıcı için kanıt oluşturduğumuz web tarafına. Burada yapacağımız şey çok basit. Whitelist’imizden ağaç oluşturup, web sitemize bağlanan cüzdan adresine özel bu ağaçtan kanıt oluşturuyoruz. Bu kanıtı bağlanan adrese verip kontratla iletişime geçmesini sağlıyoruz.
Web arayüzünden kontratla iletişime geçmek çok daha geniş bir konu olduğu için bugün sizlere sadece basit bir html dosyadan tarayınızın konsoluna ağacınızın kökünü ve kanıtımızı yazdıracağız. Daha sonra bunu etherscan üzerinden kontrata göndererek test edebilirsiniz.
Kod aşağıdaki gibidir. Bu kodda kullanılan kütüphaneler merkletree.js, keccak256.js ve ethers.js kütüphaneleridir.
const { keccak256 } = ethers.utils
// Listemiz (Kısaltılmış hali. Tam hali github'dadır. Adresler rastgeledir)
const whitelist = ['0xa44c743bf3105ae25c94d870b9913a346eabf10f','0xaee2b529d29aa9cd4a56d9778ce088837c4b2e0a','0xfb1175cfe5b6b39d30917cbdfd08fa82165aa7ae'........]
// Adres verilerimizi hash'ini alarak yapraklara (leaves) çeviriyoruz
const leaves = whitelist.map(x => keccak256(x))
// Merkle ağacımızı oluşturuyoruz
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true })
// Ağcımızın kökünü konsola yazdırıyoruz. Bu kökü kontrata yüklememiz gerekiyor.
console.log("Root: " + tree.getHexRoot())
// Çıktı: 0x235a431d30b7cc19b656d1ef14a6c5a257aab377a5854800c2d5446a1d3beb33
// web sitemize bağlanan cüzdanın adresinden merkle yaprağı (leaf) üretiyoruz
const leaf = keccak256("0x5831240fd3f7593f627da552fe7ddd60e9685723")
console.log("leaf: " + leaf)
// Çıktı: 0x42984d0c33ff73f39c28bc099e07c5717f7083992ea041a856a7f2b69564eed5
// Yaprağı kontrata vermiyoruz. Sadece nasıl bir çıktısı olduğunu görmek için paylaştım.
// Bu yaprağı kullanarak bu adrese özel kanıt üretiyoruz. Eğer adres ağaçta (listede) yoksa proof üretilemez.
const proof = tree.getHexProof(leaf)
// Kanıtımız hazır. Kontrata göndereceğimiz kanıt budur. Gördüğünüz gibi kanıtımız bir bytes32[] dizisidir.
console.log(proof)
// Çıktı ['0x4f0cb1eccc367bed452a2941a64740aff2227db53dd7139ba9c3b54f2e4822c0', '0x1a6b8e480789893ee8a77417603ca1c1f9f72aeba6defb7f6ddb60c78841da96', '0x8e424c205c169cdb86c872995fcb163d2fb246999e13fa2bcba7e771b1dc8d1f', '0x9953a72447b19073e3f67d7050896aefb08820c53efc8f0c8af89c14e3eb0e2a']
// Bu kanıtı direkt etherscan üzerinden kontrata manuel olarak verecek isek dizi elemanlarının etrafındaki tırnak '' işaretlerini silmemiz gerekiyor. [eleman1, eleman2, eleman3] şeklinde.
Yukarıda gördüğünüz kodların çok çirkin durmasına bakmayın. Çoğu sizin için yazdığım yorum satırı. Orada iş yapan sadece 6 satır kod var!
Kendi projelerinizde de böyle detaylı olmasa da yorum satırları yazarak ilerlemenizi şiddetle tavsiye ederim. Çünkü o projeye uzun bir aradan sonra dönüp baktığınızda nerede neyi nasıl yaptığınızı siz bile çözemeyebilirsiniz.
Yorum satırları hem size yardımcı olacaktır, hem de birlikte çalıştığınız ekip arkadaşlarınıza. Ayrıca açık kaynak kodlu bir yazılım yapıyorsanız, yorum satırları kesinlikle olmazsa olmazlardandır. Tabi ki de sade ve öz bir şekilde yazılması daha uygundur.
Farklı Kullanım Alanları
Yeni bir projede testlere katılımı teşvik etmek için belirli bir token ödülünü whitelist ile testlere katılan adreslere dağıtabilirsiniz.
Mevcut projenizde (oyun olabilir) periyodik olarak ödül dağıtımını whitelist ile yapabilirsiniz.
Snapshot özelliği olmayan ERC20 tokenlarında belirli bir meblağ ve üstünü elinde tutan adresleri block explorer’dan çekerek bunları bir whitelist’e atıp, o token’ı tutanlara airdrop atabilirsiniz.
Çoklu whitelist oluşturarak farklı gruplara farklı mint fiyatlandırması yapmak veya farklı gruplara farklı sayılarda free mint dağıtmak. Github linkinde kod örneğini verdim.
Merkle tree’ler gerek whitelist olarak gerekse farklı modellemeler ile projelerinizde kullanabileceğiniz harika yapılardır. Milyonlarca adresi 64 karakterle blockchain üzeride depolamamızı sağlayak bu harika mimariyi eminim çok faydalı işlerde kullanabilirsiniz.
Umarım faydalı bir yazı olmuştur. Bu yazıya dair yorumlarınızı ve yazılmasını istediğiniz diğer konuları yorumlar kısmından bize iletebilirsiniz. Bir başka yazıda görüşmek üzere, sağlıcakla kalın!
~ Bora
Teorisinden sonra uygulama gelmiş elinize sağlık.