Solidity ile Kontratlar Arası Haberleşme
Bir kontratın başka bir kontrattaki fonksiyonları çağırarak hedef kontratın state bilgisini okumak ve istenirse bu fonksiyonlarla state'i değiştirmek.
Merhabalar, bu yazımızda kontratlar arası haberleşmenin nasıl yapıldığını ele alacağız. Bu konuyu araştırdığınızda her yerde bulacağınız şey iletişim halinde olacak olan iki kontratın aynı anda deploy edilmesi şeklinde olacaktır ve bunun sizin aradığınız bir şey olduğunu düşünmüyorum. Sonuçta farklı adrese sahip bambaşka bir kontratla değil, aynı adrese sahip kontratlarla iletişime geçiliyor.
Bugün burada biz tamamen birbiri ile alakasız kontratlarla nasıl iletişime geçeceğimizi göreceğiz. Tüm kodlara buradaki Github linkinden erişebilir ve Remix üzerinden kendiniz de deneyebilirsiniz.
Basit bir örnekle başlayalım. Diyelim ki biz oluşturduğumuz oyun içerisindeki nesneleri (items) native token (ether, avax, etc.) ile değil de kendi oyunumuzun tokenı ile satmak istiyoruz. Bu durumda bizim nesnelerimizin kontratı (ERC1155) ile token kontratımız (ERC20) iletişime geçecekler. Nesne kontratımız, bir nesne mint edilirken token kontratımıza gidip bu mint eden adamın şu kadar tokenını bana ver diyecek. Bunu yapmak için 4 yol var.
1- Standart kontratlarla iletişime geçme
Aşağıda gördüğünüz gibi tokenContract isimli address tipindeki değişkenimizi ERC20 kontratı ile sarıyoruz ve ERC20 kontratında bulunan herhangi bir fonksiyonu çağırarak işlemimizi yaptırıyoruz. Aşağıdaki kod satırına buradan ulaşabilirsiniz.
ERC20(tokenContract).transferFrom(_msgSender(), address(this), 25 ether * amount);
Dikkat ederseniz transfer()
fonksiyonu yerine transferFrom()
fonsiyonunu kullandık. Çünkü token kontratı ile iletişime geçen adres mintleyen kişi değil, bizim nesne kontratımız. Nesne kontratımız token kontratına gidiyor, mintleyen kişinin adresini verip, kendisine (address(this)
) 25 adet token göndermesini istiyor.
Not: Fonksiyon parametresinde gördüğünüz 25 ether 25 token anlamına geliyor. Ether bir birim demek. Wei, gwei, ether nedir araştırınız.
Fakat bunu Remix ile denediğinizde işleminizin fail olduğunu göreceksiniz. "ERC20: insufficient allowance" hatasını döndürecektir. Bunun sebebi nesne kontratımızın mintleyen kişinin tokenlarını kendisine transfer ettirebilmesi için buna izninin olması gerekmesidir. Yoksa herkesin cüzdanı boşaltılırdı 😅
Dolayısıyla eğer mintleyen kişinin nesne kontratına kullanım izini (allowance) vermediyse, oyun içerisinden önce token kontratımızla direkt iletişime geçirip increaseAllowance()
fonksiyonu ile nesne kontratımıza yeterli miktarda izin verdireceğiz, daha sonrasında da nesne kontratımızın mint fonksiyonunu çağıracağız.
Aynı kullanıcı 1 kere allowance’ı yüksek bir miktar verdikten sonra bir dahaki seferlere direkt nesne kontratımızla iletişime geçip mintleyecektir. Bu DeFi platformlarında bir token swap yapacağınız zaman önce Approve, sonra Swap yapmanız ile birebir aynı işlemdir.
Mutlaka Github linkinden kodları ve kodlarla birlikte yazmış olduğum yorum satırlarını okumanızı ve incelemenizi tavsiye ederim.
2- Encode/Decode ederek özel fonksiyonlarla iletişime geçme
Yukarıdaki örnekte ERC20 gibi sabit bir kontratın kullanımını gösterdik. Gördüğünüz gibi son derece basit tek satırlık bir kod. Bunu ERC721 ve ERC1155 gibi diğer kontratlarla da kullanabilirsiniz. Peki standart fonksiyonlar haricinde kendi oluşturduğunuz fonksiyonları nasıl çağırabilirsiniz?
ContA ve ContB adında 2 kontratımız olsun. ContA ana kontratımız. name ve cost adında değişkenlere sahip. ContB ise bu ContA’dan bu verileri okuyabiliyor ve bu verileri değiştireibliyor. Tabiki de bu fonksiyonlar public olduğu için herkes değiştrebilir ama biz ContB ile değiştireceğiz.
ContB getInfo_B()
fonksiyonu ile ContA’dan veriyi okuyor, updateInfo_B()
fonksiyonu ile de bu veriyi değiştiriyor. Yani state’i değiştiriyor. Başka kontrattan veri alırken önce iletişime geçeceği fonksiyonu abi.encodeWithSignature()
ile encode ediyor sonra bunu contractA.call()
yaparak hedef kontrata yolluyor. Bu yollama işleminin (transaction) fail olup olmadığını bir boolean ile kontrol edip geri çıktı olarak dönen veriyi returnData değişkeninde saklıyor. Daha sonra returnData’yı decode ederek içindeki verileri çıkartıyor ve istediği gibi kullanıyor. Bu örnekte localName ve localCost adını verdiğim ContB’ye ait değişkenlere atadım.
Kodlara yazdığım yorum satırlarını okursanız her şey gayet anlaşılır olacaktır diye umut ediyorum. Kodlara direkt erişimi buradaki Github linkinden sağlayabilirsiniz.
Veri okumak yerine veri yazmak istersek de birebir aynı şeyleri tekrarlıyoruz. Hedef fonksiyonu parametreleri ile yazıyoruz, kendi girdi verimizi veriyoruz, kontratla iletişime geçiriyoruz, eğer transaction success olursa, işlem tamam, ContA’daki veri değişmiş oluyor.
Burada dikkat etmeni gereken şey, iletişime geçilen fonksiyon ve onun parametreleri. Hedef kontrattaki ile birebir aynı şekilde girmeniz gerekiyor ve birden fazla parametre yollarken yorum satırlarında dikkatinizi çektiğim formatta kullanmanız gerekiyor.
Kullanımı bu şekilde, bununla neler yapabileceğiniz ise tamamen sizin ihtiyaçlarınıza ve tasarımınıza kalmış. Sonuçlar aşağıdaki gibidir:
3- Interface oluşturarak özel fonksiyonlarla iletişime geçme
Yukarıda yaptığımız işlemin aynısını ContA’nın bir arayüzünü oluşturarak da kullanabiliriz. Bu durumda payload olsun, abiEncode, Decode vs olsun hiçbirine gerek kalmadan çok temiz bir şekilde veri alıp, veri yazabileceğiz. Eğer çok fazla yerde bu kontratlar arası etkileşimi yapıyorsanız hem yazılan kodu kısaltma, hem de daha okunabilir yapmak adına interface kullanmak en iyi yoldur.
Öncelikle aşağıdaki gibi ContA’nın bir interface’ini oluşturuyoruz. Eğer ContA bu örnekteki gibi 2 fonksiyon değil çok daha fazla fonksiyona sahip olsa bile siz interface’e sadece iletişime geçeceğiniz fonksiyonları ekleyin. Geri kalanını eklemenize gerek yok çünkü iletişime geçmeyeceksiniz.
Gördüğünüz gibi interface’i yukarıda oluşturduk, interface olduğu için fonksiyonların içi boş. Sadece 1 satır her fonksiyon. Aşağıda kullanırken de sanki kendi fonksiyonuymuş gibi direkt çağırıyoruz. Tek satırda iletişime geçiyoruz. 3-4 satırlık çirkin bir kod 1 satırlık temiz bir koda dönüştü. Hem daha az, hem daha temiz. Mis.
ContA birebir aynı kaldığı için görüntüsünü paylaşmıyorum, ContB’nin interface’le kullanımı yukarıdaki gibi. Yorum satırlarını sildiğinizde aslında bir avuç kod var. Sonuçlar birebir aynıdır.
Dikkat: Remix’de test ederken ContB’de contractA olarak girdiğimiz ContA’nın adresini girmeyi unutmayın! Dolayısıyla önce ContA’yı deploy edin, sonra ContB’yi de deploy edilmiş ContA’nın adresini girerek deploy edin. Yani ConB, ContA’nın adresini bilsin ki iletişime geçebilsin. Unutmanız çok muhtemel ve normal!
4- Tüm fonksiyonları birleştirerek özel fonksiyonlarla iletişime geçme
Aslında buna hiç değinmeye gerek görmüyorum fakat internette en yaygın gösterilen kullanım şekli bu. İronik bir şekilde bu örneklerde farklı kontratı çağırıyorlar fakat AYNI adreste deploy edilmiş farklı kontratı çağırıyorlar. Yani aslında farklı bir class gibi kullanıyorlar “başka kontrat” dedikleri kontratı.
Chainlik’ten Alchemy’e kadar çoğu yerde hep bu örnek var. Bu örnekleri “Solidity how to call another smart contract” diye aratarak bulabilirsiniz. Başka bir kontratla iletişimden bahsettiğimiz için ben bu örneği de farklı adrese sahip bambaşka bir kontratla iletişim şekilde ele alacağım.
ContA yine aynı kalıyor, ContB’nin olduğu sayfaya direkt ContA’yı kopyala yapıştır yapıyoruz. Kullanımı Interface’deki örnek ile aynı. Tek fark IContA yerine direkt ContA yazarak ContA değişkenimizi oluşturuyoruz.
Bu internetteki yaygın kullanımının farklı kontratla iletişime geçme versiyonuydu. Fakat bizim iletişime geçtiğimiz asıl ContA farklı bir adreste olduğu için, aslında bizim buraya yapıştırdığımız ContA’nın içindeki değişkenlere ve işlemlere hiç ihtiyacımız yok. Yani bu gördüğünüz koddaki ContA’yı da bu şekilde çevireibliriz:
Fark ettiyseniz interface’de de, contract versiyonunda da tek ihtiyacımız olan şey fonksiyonu tanımladığımız o başlığı. Çünkü biz farklı bir adresteki kontratla iletişime geçerken bizim kontratımız bu başlıklara bakarak nasıl iletişime geçeceğini çözüyor. Contract yerine Interface şeklinde kullanmak yine daha yerinde bir kullanım olacaktır.
Özet
Farklı kontraları birbiri ile iletişime geçirmek için 4 farklı yöntem görmüş olduk.
Standart kontratlarla iletişime geçme (ERC20, ERC721, ERC1155, vs.)
Encode/Decode ederek özel fonksiyonlarla iletişime geçme
Interface oluşturarak özel fonksiyonlarla iletişime geçme
Tüm fonksiyonları birleştirerek özel fonksiyonlarla iletişime geçme
Özel fonksiyonlarla iletişime geçiyorsak, bunu en temiz kullanmanın yolu hedef kontrattın bir interface yani arayüzünü oluşturarak iletişime geçmektir. Kontratların birbiri ile haberleşmesini çok çeşitli sebeplerle kullanabilirsiniz. Belirli token holderlarına, NFT sahiplerine airdrop dağıtabilir, merkezsiz borsalardaki liquidity' pool’lardan havuz bilgisini çekerek oracle’a gerek kalmadan fiyat bilgisini alabilir yada tamamen kendi projeniz içerisindeki bir tasarımda kullanabilirsiniz.
Neler yapabileceğiniz tamamen size kalmış. Umarım faydalı bir yazı olmuştur. Geri bildirimlerinizı yorumlar kısmından bize iletebilirsiniz. Daha üretken yarınlara, hoşça kalın!
~ Bora