"Enter"a basıp içeriğe geçin

Ethereum VM Uyumlu Token Oluşturma ve .Net Projesine Entegre Etme

Bu yazıda baştan sona kadar Smart Contract(akıllı sözleşme) ile BSC üzerinde çalışan ve BEP-20 standardını kullanan bir token yazacağız. BSC TestNet’e deploy edeceğiz. Daha sonra ise .Net 5 Web Uygulaması’na entegrasyonunu sağlayacağız. Ethereum VM uyumlu token oluşturma ‘ya
Başlamadan önce birkaç konuya kısaca değinelim.

BSC Nedir?

BSC’nin açılımı Binance Smart Chain’dir. EVM ( Ethereum Sanal Makinası) ile uyumlu çalışmaktadır. Bu özelliğinden dolayı da Ethereum tabanlı çalışan uygulamaları kendi üzerine çekmeyi kolaylaştırmaktadır. Üzerinde merkeziyetsiz uygulamalar koşturabilirsiniz. Ethereum’a göre tx fee çok daha ucuzdur. Genellikle tercih edilme sebeplerinden biri de budur. Daha fazla detaya buradan bakıp, yazıya öyle devam edebilirsiniz.

BEP-20 Nedir?

Sözleşmelerin uyumlu olabilmesi için bir takım fonksiyon ve olayları barındırmaları gerekmektedir. BEP-20 de BSC üzerinde çalışacak sözleşmeler için oluşturulmuş bir standarttır. Ethereum’daki ERC-20 ile aynıdır.

Fonksiyonlar (function)

  • totalSupply : Toplam token arzı deklare edilir.
  • balanceOf : Hesaptaki token miktarı.
  • name: Token adı.
  • symbol: Token sembolü. Örneğin BTC, ETH, LTC, …
  • decimal: Ondalık belirleyici yani sıfırın sağında kaç basamak olacağı.
  • transfer: Transfer işlemini yapar. Burada token sahibi, işlemi yapan kişidir.
  • transferFrom: Token sahibiymiş gibi transfer yapmayı sağlar.
  • approve: İşlem onayı.
  • allowance: İzin verilen tutarı döndürür.
  • getOwner: Sözleşme sahibi

Olaylar (event)

Blok zincirine olay ekler.

  • Transfer: “transfer” yapıldığında tetiklenmelidir.
  • Approval: “approve” yapıldığında tetiklenmelidir.

Geliştirme Araçları ve Gereksinimler

Yukarıdaki maddelerin üzerine ihtiyacınız olacak linkleri bıraktım, tıklayıp erişebilirsiniz.

Token Özelleştirme – İş Öyküsü

Oluşturacağımız token için kendi iş modelimize özgü yetenekler eklemek isteyebiliriz.

Bu örnekte, sözleşmeye bir “Komisyon Hesabı” ekleyip yapılan her transferin yüzde 3’ünü bu hesaba aktaracak şekilde işlem yapacağız. Ayrıca komisyon hesabından veya komisyon hesabına yapılan transferleri bu işlem ücretinden muaf tutacağız.

Hadi başlayalım!!!

Metamask

Öncelikle Chrome Web tarayıcısına Metamask cüzdanımızı kuruyoruz. Kurulum detaylarından teker teker bahsetmeyeceğim internette bir çok yazılı ve videolu anlatımı mevcut. Daha önce yapmamış olanlar için buraya bir yardımcı link bırakıyorum.

Metamask’a RPC Endpoint Ekleme

Biz, BSC RPC TestNet üzerinde çalışacağız.
Bilgileri şunlar (bu linkten de bakabilirsiniz)

BSC RPC Testnet Endpoint: https://data-seed-prebsc-1-s1.binance.org:8545/
ChainID:97
Symbol:BNB
BlockExplorerURL: https://testnet.bscscan.com

Metamask -> Settings -> Networks -> Add a network adımlarını izleyerek tüm alanları aşağıdaki gibi doldurun.

BSC RPC Endpoint Testnet

Daha sonra oluşturduğumuz bu networke geçip kullanacağımız 3 hesabı oluşturun. Ben şu isimleri veriyorum sırasıyla.

  1. OwnerAccount -> Sözleşmeyi bu hesap ile oluşturacağız ve tüm tokenlar bu hesaba tanımlanacak.
  2. TransferAccount -> Test için bu hesaba transfer yapacağız.
  3. CommissionAccount -> Transfer işlemlerinden doğacak olan komisyonu bu hesaba yansıtacağız.

Son hali bu şekilde:

İşlem yapabilmek için BNB’ye ihtiyacımız olacak. Onun için de şuradan OwnerAccount adresinize BNB gönderebilirsiniz.

Remix Ethereum Solidity IDE

Buraya kadar her şey tamamsa Smart Contract’ımızı yazmak için Remix‘i açıyoruz.

Açılan sayfada FILE EXPLORERS’da “contracts” klasörüne sağ tıklayarak contract dosyamızı oluşturuyoruz.

Buraya kadar geldiyseniz Solidity dokümanına genel olarak göz atmanızı öneririm.
Örneğin Function Visibility(public, external, internal, private), Block and Transaction Properties(msg.sender) başlıklarından başlayabilirsiniz.

Ayrıca Solidity’nin tam olmasa da Java ve Javascript’i andıran yanları dar var diyebiliriz.
-Nasıl yani? derseniz.
Yazılım dinamikleri ve yaklaşımı açısından Java, syntax olarak da Javascript.
Başka şeylere benzetenler varsa da yorumlara bekleriz. 🙂

Solidity ile Smart Contract Yazma

İlk olarak lisans commentini ekliyoruz. Eklemez iseniz compiler warning verir.

// SPDX-License-Identifier: GPL-3.0

Solidity Version

pragma solidity ^0.8.11; //Bu yazıyı hazırlarken en son sürüm buydu.

Yazının başında bahsettiğimiz BEP-20 Standardı için belirttiğimiz function ve eventleri IBEP20 ismini verdiğimiz bir interfacede toplayalım.

interface IBEP20 {
//functions
  function name() external view returns (string memory);
  function symbol() external view returns (string memory);
  function decimals() external view returns (uint8);
  function totalSupply() external view returns (uint256);
  function balanceOf(address _owner) external view returns (uint256);
  function getOwner() external view returns (address);
  function transfer(address _to, uint256 _value) external returns (bool);
  function transferFrom(address _from, address _to, uint256 _value) external returns (bool);
  function approve(address _spender, uint256 _value) external returns (bool);
  function allowance(address _owner, address _spender) external view returns (uint256);

//events
  event Transfer(address indexed _from, address indexed _to, uint256 _value);
  event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}

Matematiksel işlemlerimizi bir arada tutmak ve özellikle de overflow saldırılarını önlemek adına kullanacağımız methodlarımızı aşağıdaki gibi bir libraryde topluyoruz. Ayrıca bu Bu libraryi googlelayıp da bulabilirsiniz. Yazının en sonunda yararlandığım linkleri ve projenin son halini paylaşacağım.

library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");
        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }

    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        uint256 c = a - b;
        return c;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }
        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");
        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }

    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b > 0, errorMessage);
        uint256 c = a / b;
        return c;
    }

    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return mod(a, b, "SafeMath: modulo by zero");
    }

    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b != 0, errorMessage);
        return a % b;
    }
}

Şimdi, iş öykümüze göre Smart Contractımızı kodlamaya geçelim.

Contract adını verip interfacei tanıtıyoruz.

contract YazilimNedir is IBEP20{
}

Math Library’i de tanıtalım.

contract YazilimNedir is IBEP20{
    using SafeMath for uint256;
}

Contract içerisinde ihtiyacımız olacak variableları declare ediyoruz.

address private _ownerOfContract; //Token sahibini burada tutatacağız
string private _name = "YazilimNedir"; //Token adı
string private _symbol = "YZLMNDR"; //Token sembolü
uint8 private _decimals = 4; //Ondalık basamak sayısı. Örn:1000000000.0000
uint256 private _totalSupply = 10000000000000; //Toplam arz
mapping(address=>uint256) private _balances; //Token holderlarını burada tutuyoruz.
mapping (address => mapping (address => uint256)) private _allowances;
uint8 private _commissionPercent = 3; //Komisyon Yüzdesi
address private constant _commissionWallet = 0x792b262e797B77eD8d2eEcEF2Be4e540648f5c51; //Komisyon Hesabı. Metamask'da oluşturmuştuk. 

Solidty’de “mapping” Nedir?

KeyType-ValueType koleksiyonlar oluşturulmasını sağlar.

Constructor(yapılandırıcı) methoddaki işlemlerimiz şu şekilde olacak.

constructor()
{
    _ownerOfContract = msg.sender;
    _balances[_ownerOfContract] = _totalSupply; //Arzı ana hesaba tanımlıyoruz.
    emit Transfer(msg.sender, _ownerOfContract, _totalSupply); //Transfer olayını blockhain'e       //bildiriyoruz. IBEP20'de tanımladığımız Transfer event tetiklenir. 
//Bunları https://testnet.bscscan.com/address/{SözlesmeAdresi} üzerinden Event sekmesindeki //loglardan görebilirsiniz.
}

Solidity’de “msg.sender” Nedir?

Contract’a bağlantı kuran aktif hesap bilgisini verir.

Solidity’de “emit” Nedir?

emit, blokzincirine event ekler ve bu event, merkeziyetsiz uygulamalar (dApp) tarafından okunabilir.

Ve son olarak functionları implemente ediyoruz.

    function name() external view returns (string memory){
        return _name;
    }

    function symbol() external view returns (string memory){
        return _symbol;
    }

    function decimals() external view returns (uint8){
        return _decimals;
    }

    function totalSupply() external view returns (uint256){
        return _totalSupply;
    }

    function balanceOf(address _owner) external view returns (uint256){
        return _balances[_owner];
    }

    function getOwner() external view returns (address){
        return _ownerOfContract;
    }

    function transfer(address _to, uint256 _value) external returns (bool){
        _transfer(msg.sender, _to, _value);
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal {
        require(sender != address(0), "BEP20: transfer from the zero address");
        require(recipient != address(0), "BEP20: transfer to the zero address");

        if(_takeCommission(sender, recipient)){
            _transferWithCommission(sender, recipient, amount);
        } 
        else{
            _transferWOCommission(sender, recipient, amount);
        }
    }

    function transferFrom(address _from, address _to, uint256 _value) external returns (bool){
        _transfer(_from, _to, _value);
        _approve(_from, msg.sender, _allowances[_from][msg.sender].sub(_value, "BEP20: transfer amount exceeds allowance"));
        return true;
    }

    function approve(address _spender, uint256 _value) external returns (bool){
        _approve(msg.sender, _spender, _value);
        return true;
    }

    function _approve(address owner, address spender, uint256 amount) internal {
        require(owner != address(0), "BEP20: approve from the zero address");
        require(spender != address(0), "BEP20: approve to the zero address");

        _allowances[owner][spender] = amount;
        emit Approval(owner, spender, amount);
    }

    function allowance(address _owner, address _spender) external view returns (uint256){
        return _allowances[_owner][_spender];
    }

    function _transferWithCommission(address sender, address recipient, uint256 amount) private
    {
         _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance");
         uint256 _fee = _calcCommission(amount, _commissionPercent);
         _balances[_commissionWallet] = _balances[_commissionWallet].add(_fee);
         _balances[recipient] = _balances[recipient].add(amount.sub(_fee));
         emit Transfer(sender, recipient, amount.sub(_fee));
    }

    function _calcCommission(uint256 value, uint8 commissionPercent) private pure returns(uint256)
    {
        return value.mul(commissionPercent).div(100);
    }

    function _transferWOCommission(address sender, address recipient, uint256 amount) private
    {
        _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance");
        _balances[recipient] = _balances[recipient].add(amount);
        emit Transfer(sender, recipient, amount);
    }

    function _takeCommission(address sender,address recipient) private pure returns(bool)
    {
        if(sender == _commissionWallet || recipient == _commissionWallet){
            return false;
        }
        else{
            return true;
        }
    }

“_transfer” fonksiyonu içerisinde “_takeCommission” ile iş öykümüzde belirttiğimiz kurala göre transfer işleminden komisyon alınıp alınmayacağına karar veriyor. Buna göre “_transferWithCommission” veya “_transferWOCommission” fonksiyonu çağrılıyor. “_calcCommission” komisyon hesabına eklenecek tutarı döndürüyor. Ardından _balance üzerinde gönderici’den token eksiltilirken, alıcı ve komisyon hesaplarına ilgili eklemeler yapılıyor. Son olarak da Transfer event tetikleniyor.

Remix – Solidity COMPILER

IDE ekranında sol menüden compiler’ı açın. Ayarlar şu şekilde.

Remix IDE Solidity Compiler

.Net uygulamamızda kullanmak için daha sonra dönüp buradan “ABI” datasını kopyalayacağız. Şimdilik buraya kadar herhangi bir error yoksa Deploy kısmına geçebiliriz.

Remix – Solidity Deploy

Bu ekranda Environment “Injected Web3” olacak ve account için de Metamask‘da oluşturduğumuz OwnerAccount‘u seçeceğiz. (Eğer bu hesaba BNB göndermediyseniz bu işlemi yapmadan önce metamask kısmında linkini verdiğim siteye gidip OwnerAccount’unuza BNB göndermeniz gerekiyor.)

smart contract deploy on remix

Deploy dedikten sonra Metamask bir onay isteyecek ve onu da onayladıktan ortalama birkaç saniye içerisinde SmartContract’ınız BSC-TestNet’e deploy olmuş olacak.

Metamask Token Import

Oluşturduğumuz tokeni metamask üzerinde görmek için:
Öncelikle IDE’den oluşan contractın yanındaki ikona tıklayıp adresini kopyalayalım.
Daha sonra Metamask’ı açıp hesap ekranının en altın “import token” yazısına tıklayıp, kopyaladığımız contract adresini yapıştırıyoruz. Aşağıdaki pasif alanlar otomatik dolacaktır.

Şimdi birkaç işlem yapalım…

*Öncelikle OwnerAccount balance'ına bakalım. 
*Daha sonra TransferAccount'a 10000 token gönderelim.
*CommissionAccount'a bu transferin yüzde 3'ünün yansıyıp yansımadığına bakalım.

Deployed Contract kısmından oluşturduğumuz contractın üzerine gelip genişlettiğimizde dışarıya açmış olduğumuz fonksiyonlarımız listelenecek.

  1. OwnerAccount BalanceOf ->
ethereum uyumlu token oluşturma

2. Transfer: OwnerAccount’dan TransferAccount’a:

Token transfer on Remix IDE

Yukarıda fark ettiyseniz 10000 gönderelim demiştik ama “_value” alanına 100000000 girdik. Bunun sebebi contractda decimal değerini 4 olarak belirlemiş olmamız. Eğer bu transferi Remix IDE üzerinden değil de direk Metamask üzerinden gerçekleştirseydik transfer miktarı olarak 10000 girmemiz yeterli olacaktı.

TransferAccount’un balanceına bakalım:

ethereum uyumlu token oluşturma

3.CommissionAccount balance kontrolü:

get balance of account
balance show on metamask

.NET Web Uygulaması ile ETHEREUM tabanlı Smart Contract İmplementasyonu Yapmak

Buraya kadar ne yaptık?
Ethereum Virtual Machine ile çalışan Binance Smart Chain Test Network’ü üzerinde BEP-20 standardında ve kurallarını kendimize göre özelleştirdiğimiz bir token oluşturduk.

Buradan sonrası C# bilgisi gerektirmektedir.

Şimdi bu tokene erişebileceğimiz bir .NET 5 Web uygulaması yapalım ve fonksiyonlarımızı çağıralım.

.Net 5 Web Application ne yapacak?

Örnek olarak bir Dashboard sayfası oluşturup buraya CommissionAccount bakiyesini yazdıralım.

Projeyi oluşturma adımlarını geçiyorum…

NuGet Package’dan “Nethereum.Web3” kütüphanesini kuruyoruz.

Nethereum.Web3 NuGet Package

Remix IDE’ye geri dönüp “Solidity Compiler” kısmından ABI’yi kopyalıyoruz.
Ardından projemize “contractabi.json” adında bir dosya oluşturup ABI’yi içerisine yapıştırıyoruz.

Remix Compiler ABI code
ethereum uyumlu token oluşturma

Solidity ABI Json Nedir?

ABI (Application Binary Interface), basitçe sözleşmelerin imzalarını taşıyan, onları tanımlayan ve açıklayan bir şemadır.

appsettings.json dosyamıza şunları ekliyoruz:

{
  "RPCEndpoint": "https://data-seed-prebsc-1-s1.binance.org:8545/",
  "ContractAddress": "0xfFE32d4b3A13A7afE89e5A2AaADfe598352e924D",
  "CommissionWalletAddress": "0x792b262e797B77eD8d2eEcEF2Be4e540648f5c51"
}

Modellerimizi ekleyelim.

public class ConfigContractABI
{
    public readonly string JsonStr;
    public ConfigContractABI(string JsonStr)
    {
        this.JsonStr = JsonStr;
    }
}

public class DashboardViewModel
{
    public CommissionWalletModel CommissionWalletData { get; set; }
}

public class CommissionWalletModel
{
    public double FormattedBalance { get; set; }
    public string Symbol { get; set; }
}

Daha sonra Startup.cs’i bu şekilde yapılandırıyoruz.

services.AddMemoryCache();
services.AddSingleton(new ConfigContractABI(File.ReadAllText("contractabi.json")));
services.AddTransient<BSCTokenService>();

BSCTokenService.cs kodu bu şekilde olacak. Github linkini en sonda paylaşacağım kaynak kodlarına oradan erişebilirsiniz.

public class BSCTokenService
{
        private const string DECIMALS_KEY = "contract.decimals";//memory cache key
        private const string SYMBOL_KEY = "contract.symbol";//memory cache key

        private string contractABI;
        private string rpcEndpoint;
        private string contractAddress;

        private readonly IMemoryCache _cache;
        
        public BSCTokenService(IConfiguration configuration, 
            ConfigContractABI configContractABI,
            IMemoryCache cache)
        {
            _cache = cache;
            rpcEndpoint = configuration.GetValue<string>("RPCEndpoint");
            contractABI = configContractABI.JsonStr;
            contractAddress = configuration.GetValue<string>("ContractAddress");
        }

        public async Task<string> GetSymbol()
        {
            string symbol;
            if(_cache.TryGetValue(SYMBOL_KEY, out symbol))
                return symbol;

            Web3 web3 = new Web3(rpcEndpoint);
            var contract = web3.Eth.GetContract(contractABI, contractAddress);
            var symbolFunction = contract.GetFunction("symbol");
            symbol = await symbolFunction.CallAsync<string>();
            _cache.Set(SYMBOL_KEY, symbol, TimeSpan.FromDays(365));
            return symbol;
        }

        public async Task<int> GetDecimal()
        {
            int decimals;
            if (_cache.TryGetValue(DECIMALS_KEY, out decimals))
                return decimals;

            Web3 web3 = new Web3(rpcEndpoint);
            var contract = web3.Eth.GetContract(contractABI, contractAddress);
            var decimalsFunction = contract.GetFunction("decimals");
            decimals = await decimalsFunction.CallAsync<int>();
            _cache.Set(DECIMALS_KEY, decimals, TimeSpan.FromDays(365));
            return decimals;
        }

        public async Task<BigInteger> GetBalanceOf(string address)
        {
            Web3 web3 = new Web3(rpcEndpoint);
            var contract = web3.Eth.GetContract(contractABI, contractAddress);
            var balanceFunction = contract.GetFunction("balanceOf");
            var balance = await balanceFunction.CallAsync<BigInteger>(address);
            return balance;
        }
}

Kodu biraz inceleyecek olursak GetSymbol ve GetDecimal ve GetBalanceOf methodlarımız bulunuyor.

symbol ve decimal değerlerini statik olarak da yazabilirdik fakat dinamik kullanımı da göstermek istedim. Cache eklememizin sebebi de keza bu. Her istekte değişmeyen(bizim contractımıza göre) verileri çekmenin bir anlamı olmayacaktır.
Nethereum kütüphanesini kullanırken rpcEndpoint, contractABI, contractAddress ve functionName verilerini sağlamamız bizim için yeterli şu anlık.

Balance formatı için extensionımızın kodu da bu: Balance değerini, decimal değeri kadar digitler.

public static class BalanceExtensions
{
    public static double ToFormatted(this BigInteger balance, int decimals)
    {
        return (ulong)balance / (Math.Pow(10, decimals));
    }
}

Son olarak Controller ve Viewları da oluşturuyoruz.

public IActionResult Index()
{
    var resultModel = new DashboardViewModel();

    var balance = tokenService.GetBalanceOf(this.configuration.GetValue<string>("CommissionWalletAddress")).Result;
    string symbol = tokenService.GetSymbol().Result;
    var decimals = tokenService.GetDecimal().Result;

    resultModel.CommissionWalletData = new CommissionWalletModel { Symbol = symbol, FormattedBalance = balance.ToFormatted(decimals) };//BalanceExtension burada kullanılıyor.
    return View(resultModel);
}
@model YazilimNedirBSCToken.Models.DashboardViewModel
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">DASHBOARD</h1>

    <div class="row g-5">
        <div class="col-md-5 col-lg-4 order-md-last">
            <h4 class="d-flex justify-content-between align-items-center mb-3">
                <span class="text-primary">Fee Wallets</span>
            </h4>
            <ul class="list-group mb-3">
                <li class="list-group-item d-flex justify-content-between lh-sm">
                    <div>
                        <h6 class="my-0">Commission Wallet</h6>
                        <small class="text-muted">Balance</small>
                    </div>
                    <span class="text-muted">@Model.CommissionWalletData.FormattedBalance @Model.CommissionWalletData.Symbol</span>
                </li>
            </ul>
        </div>
    </div>
</div>

Projeyi çalıştıralım

final ss

BSC Scan üzerinden işlemleri kontrol edelim. “Go to Contract” linkiyle gidebilirsiniz.

En alttaki constructorda contractı deploy ettiğimizde gerçekleşen transfer.
En üstteki de en son yaptığımız transfer işlemi olarak görebiliyoruz.

Sonuç olarak

EVM uyumlu çalışan BEP-20 standardında bir token çıkardık ve kendi .net projemize entegre ettik.

Bunlara ek olarak bir sonraki yazımız bu yazının devamı niteliğinde olup tokenimizi ve uygulamamızı daha da geliştirip De-Fi Exchange’lerde(örn:Pancakeswap gibi) al-sat yapma ve likiditasyon gibi konuları ele alabiliriz.

Proje Linki

Faydalanılan Linkler

Blockchain kategorisindeki diğer yazılar.

İlk Yorumu Siz Yapın

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.