Solidity by Example

Oylama

Birazdan göreceğiniz sözleşme biraz karışık ancak Solidity’nin bir çok özelliğini görebilirsiniz. Göreceğiniz kontrat bir oylama kontratıdır. Elektronik oy kullanmada asıl problem oy hakkının doğru kişilere nasıl verildiği ve manipülasyonun nasıl engelleneceğidir. Bütün problemleri burada çözmeyeceğiz ama en azından yetkilendirilmiş kişilerle hem otomatik hem de şeffaf olarak nasıl oylama yapılacağını göstereceğiz.

Fikrimiz oy sandığı başına bir kontrat oluşturup her seçenek için kısa isimler vermek. Sonrasında kontratın yaratıcısı, aynı zamanda seçim başkanı oluyor, her cüzdana tek tek oy hakkı verecek.

Sonrasında cüzdan sahipleri kendilerine ya da güvendikleri bir kişiye oy verebilirler.

Oylamanın süresi dolduğunda, winningProposal() en yüksek oyu almış teklifi geri döndürecek.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/// @title Yetkili Oylama
contract Ballot {
    // Bu sonrasında değişken olarak
    // kullanılmak için oluşturulmuş kompleks bir tür
    // Tek bir seçmeni temsil eder
    struct Voter {
        uint weight; // oyun seçimdeki etki ağırlığı
        bool voted;  // true ise oy kullanılmıştır
        address delegate; // yetkilendirilecek kişinin adresi
        uint vote;   // oy verilmiş proposalın index numarası
    }

    // Tekli teklif türü
    struct Proposal {
        bytes32 name;   // kısa ismi (32 bayta kadar)
        uint voteCount; // toplam oy miktarı
    }

    address public chairperson; // seçim başkanının adresi

    // Her adres için `Voter` (Oy kullanan kişi)
    // structına mapping (eşleştirme) değişkeni
    mapping(address => Voter) public voters;

    // `Proposal` structlarından oluşan bir dinamik dizi (dynamic array).
    Proposal[] public proposals;

    /// `proposalNames`lerden birini seçmek için bir oy sandığı oluşturur.
    constructor(bytes32[] memory proposalNames) {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;

        // Her teklif ismi için bir teklif objesi oluşturup
        // dizinin (array) sonuna ekle
        for (uint i = 0; i < proposalNames.length; i++) {
            // `Proposal({...})` geçici bir Proposal (Teklif)
            // objesi oluşturur ve `proposals.push(...)`
            // objeyi `proposals` dizisinin sonuna ekler.
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    // `voter`a bu sandıkta oy kullanma yetkisi ver.
    // `chairperson` bu fonksiyonu çağırabilir.
    function giveRightToVote(address voter) external {
        // Eğer `require`ın ilk argümanı `false`
        // gelirse işlem iptal olur ve Ether
        // harcamaları eski haline gelir
        // Eskiden bu durumda bütün gas harcanırdı
        // ama artık harcanmıyor.
        // Çoğu zaman fonksiyonun doğru çağrılıp
        // çağrılmadığını anlamak için `require`
        // kullanılırsa iyi olur
        // İkinci argüman olarak neyin hatalı olduğunu
        // açıklayan bir yazı girilebilir.
        require(
            msg.sender == chairperson,
            "Sadece chairperson yetki verebilir."
        );
        require(
            !voters[voter].voted,
            "Kişi zaten oy kullandı."
        );
        require(voters[voter].weight == 0);
        voters[voter].weight = 1;
    }

    /// Delege `to` ata.
    function delegate(address to) external {
        // referans atar
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "Oy verme yetkin yok");
        require(!sender.voted, "Zaten oy kullandın.");

        require(to != msg.sender, "Kendini temsilci gösteremezsin.");

        // Delege atamasını `to` da delege atandıysa
        // aktarır
        // Genelde bu tür döngüler oldukça tehlikelidir,
        // çünkü eğer çok fazla çalışırlarsa bloktaki
        // kullanılabilir gas'ten daha fazlasına ihtiyaç duyabilir.
        // Bu durumda, delege atama çalışmayacaktır
        // ama başka durumlarda bu tür döngüler
        // kontratın tamamiyle kitlenmesine sebep olabilir.
        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;

            // Delege atamada bir döngü bulduk, bunu istemiyoruz
            require(to != msg.sender, "Delege atamada döngü bulundu.");
        }

        Voter storage delegate_ = voters[to];

        // Oy kullanan kişi oy kullanamayan kişileri delege gösteremez.
        require(delegate_.weight >= 1);

        // `sender` bir referans olduğundan
        // `voters[msg.sender]` değişir.
        sender.voted = true;
        sender.delegate = to;

        if (delegate_.voted) {
            // Eğer delege zaten oylandıysa
            // otomatik olarak oylara eklenir
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            // Eğer delege oylanmadıysa
            // ağırlığına eklenir.
            delegate_.weight += sender.weight;
        }
    }

    /// Oy kullan (sana atanmış oylar da dahil)
    /// teklif ismine `proposals[proposal].name`.
    function vote(uint proposal) external {
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "Oy kullanma yetkisi yok");
        require(!sender.voted, "Already voted.");
        sender.voted = true;
        sender.vote = proposal;

        // Eğer `proposal` dizinin içinde yoksa,
        // otomatik olarak bütün değişiklikler
        // eski haline döner
        proposals[proposal].voteCount += sender.weight;
    }

    /// @dev Bütün oyları hesaplayarak kazanan
    /// teklifi hesaplar
    function winningProposal() public view
            returns (uint winningProposal_)
    {
        uint winningVoteCount = 0;
        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal_ = p;
            }
        }
    }

    // Kazanan teklifin indeks numarasını bulmak için
    // winningProposal() fonksiyonunu çağırır ardından
    // kazanan teklifin adını döndürür.
    function winnerName() external view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }
}

Olası İyileştirmeler

Şu an tüm katılımcılara yetki vermek için çok sayıda işlem gerçekleştirilmesi gerekiyor. Daha iyi bir yöntem düşünebiliyor musunuz?

Gizli İhale

Bu bölümde Ethereum’da tamamiyle gizli bir ihale kontratı oluşturmanın ne kadar kolay olduğunu göstereceğiz. Önce herkesin başkalarının tekliflerini görebildiği açık bir ihale kontratı oluşturup sonrasında ondan teklif süresi dolana kadar kimsenin başkasının teklifini göremediği gizli bir ihale kontratı oluşturacağız.

Basit Açık İhale

Aşağıdaki basit ihale kontratındaki fikir herkes teklif sürecinde teklfilerini gönderebilcek. Teklifler yanında teklif verenlerin tekliflerine sadık kalmaları için teklifte belirtilen parayı da içerecek. Eğer en yüksek teklif geçilirse önceki en yüksek teklifi veren kişi parasını geri alacak. Teklif süreci bittiğinde kontratlar kendi kendilerine çalışamadıklarından hak sahibi parasını almak için kontratı manuel olarak çağırılmalıdır.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SimpleAuction {
    // İhalenin parametreleri. Süreler unix zaman damgası
    // (1970-01-01'den itibaren saniyeler) ya da saniye
    // cinsinden ne kadar süreceği.
    address payable public beneficiary;
    uint public auctionEndTime;

    // İhalenin şu an ki durumu
    address public highestBidder;
    uint public highestBid;

    // Önceki tekliflerden para çekmeye izin verilenler
    mapping(address => uint) pendingReturns;

    // En son `true`ya çevir, herhangi bir değişiklik yapılmasını engeller
    // varsayılan olarak `false` tanımlanır.
    bool ended;

    // Değişikliklerde yayınlanacak Event'ler
    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

    // Başarısızları açıklayan hatalar

    // Üçlü eğik çizgiler natspec yorumları olarak
    // adlandırılır. Kullanıcıya bir işlemi onaylayacağı
    // zaman ya da bir hatada gösterilir.

    /// İhale bitti.
    error AuctionAlreadyEnded();
    /// Eşit ya da daha yüksek bir teklif var.
    error BidNotHighEnough(uint highestBid);
    /// Teklif henüz bitmedi.
    error AuctionNotYetEnded();
    /// auctionEnd fonksiyonu zaten çağrıldı.
    error AuctionEndAlreadyCalled();

    /// Hak sahibi adına `beneficiaryAddress`
    /// `biddingTime` daki süre ile bir ihale başlatır.
    constructor(
        uint biddingTime,
        address payable beneficiaryAddress
    ) {
        beneficiary = beneficiaryAddress;
        auctionEndTime = block.timestamp + biddingTime;
    }

    /// İşlemle birlikte teklifteki para da
    /// gönderilir. Ödeme sadece teklif kazanmazsa
    /// iade edilir.
    function bid() external payable {
        // Herhangi bir argümana gerek yok,
        // bütün bilgi zaten işlemin parçası.
        // payable anahtar kelimesi fonksiyonun
        // Ether alabilmesi için zorunlu.

        // teklif süreci bittiyse çağrıyı
        // geri çevir.
        if (block.timestamp > auctionEndTime)
            revert AuctionAlreadyEnded();
        // Teklif daha yüksek değilse,
        // parayı geri gönderin (revert ifadesi
        // parayı almış olması da dahil olmak
        // üzere bu fonksiyon yürütmesindeki tüm
        // değişiklikleri geri alacaktır).
        if (msg.value <= highestBid)
            revert BidNotHighEnough(highestBid);

        if (highestBid != 0) {
            // Basit bir şekilde highestBidder.send(highestBid)'i
            // kullanarak para göndermek bir güvenlik riski oluşturuyor
            // çünkü güvenilmez bir kontratı (içinde fallback fonksiyonu
            // içeren) çalıştırabilir. Her zaman katılımcıların paralarını
            // kendilerinin çekmeleri daha güvenilirdir.
            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

    /// Geçilmiş bir teklifin parasını geri çek.
    function withdraw() external returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // Bu değeri sıfıra eşitlemek önemli çünkü alıcı bu fonksiyonu
            // `send` tamamlanmadan tekrar çağırırsa (reentrancy) alması gerekenden
            // daha fazla para çekebilir.
            pendingReturns[msg.sender] = 0;

            // msg.sender `address payable` türünde değil ve `send()`
            // fonksiyonunda çağrılabilmesi `payable(msg.sender)` ile
            // `address payable` a dönüştürülmesi gerekiyor.
            if (!payable(msg.sender).send(amount)) {
                // No need to call throw here, just reset the amount owing
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

    /// İhaleyi bitir ve en yüksek teklifi
    /// hak sahibine gönder.
    function auctionEnd() external {
        // Diğer kontratlar ile etkileşime giren (fonksiyon çağıran ya da
        // Ether gönderen) fonksiyonları üç parçada şekilldenirmek güzel bir yöntem.
        // Şu parçalar
        // 1. koşul kontrolleri
        // 2. eylem gerçekleştirenler (koşulları değiştirebilirler)
        // 3. başka kontratlarlar etkileşime girenler
        // Eüer bu fazlar karışırsa, diğer kontrat bu kontratı çağırıp
        // durumları değiştirebilir ya da olayların (ether ödemesi gibi)
        // birkaç kere gerçekleşmesine sebep olabilir.
        // Eğer içeriden çağırılan fonksiyonlar başka kontratlarla etkileşime
        // giriyorsa o fonksiyonlar da başka fonksiyonlarla etkileşenler olarak
        // değerlendirilmeli

        // 1. Şartlar
        if (block.timestamp < auctionEndTime)
            revert AuctionNotYetEnded();
        if (ended)
            revert AuctionEndAlreadyCalled();

        // 2. Etkiler
        ended = true;
        emit AuctionEnded(highestBidder, highestBid);

        // 3. Etkileşim
        beneficiary.transfer(highestBid);
    }
}

Gizli İhale

Aşağıda yukarıdaki açık ihalenin kapalı ihaleye dönüştürülmüş halini bulabilirsiniz. Gizli ihalenin avantajı ihale sürecinin sonunda doğru bir zaman baskısı oluşturmaması. Saydam bir işlem platformunda gizli ihale oluşturmak çelişkili olsa da kriptografi burada yardımımıza koşuyor.

Teklif süreci boyunca, teklif veren kişi aslında gerçekten teklif yapmıyor, sadece hashlenmiş bir halini gönderiyor. Şu an hash değerleri eşit olan iki değer (yeterince uzun) bulmak pratik olarak imkansız olduğundan, teklif veren kişi bu şekilde teklif oluşturmuş olur. Teklif süreci bittikten sonra teklif veren kişiler tekliflerini açıklamalı, girdikleri şifrelenmemiş değerin hashlenmiş hali ile önceden girdikleri hash ile aynı olmalıdır.

Başka bir zorluk da gizlilik ve bağlayıcılığı aynı anda sağlamak. Teklif veren kişinin kazandıktan sonra teklifinden vazgeçmemesinin tek yolu teklif ile birlikte parayı da yollaması ancak transferler Ethereum’da gizlenemediğinden herhangi bir kişi miktarı görebilir.

Aşağıdaki kotnrat bu sorunu teklif ile birlikte herhangi bir miktar paranın birlikte gönderilmesiyle çözüyor. Miktar ile teklifin eşitliği sadece açıklama fazında ortaya anlaşılabildiği için bazı teklifleri geçersiz olabilir, ve teklif veren kişiler bunu kasıtlı olarak kullanabilir (hatta bu durum daha fazla gizlilik sağlıyor) Teklif veren kişiler kasıtlı olarak bir kaç yüksek ve düşük geçersiz teklifler oluşturarak kafa karıştırabilirler.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract BlindAuction {
    struct Bid {
        bytes32 blindedBid;
        uint deposit;
    }

    address payable public beneficiary;
    uint public biddingEnd;
    uint public revealEnd;
    bool public ended;

    mapping(address => Bid[]) public bids;

    address public highestBidder;
    uint public highestBid;

    // Önceki tekliflerden para çekmeye izin verilenler
    mapping(address => uint) pendingReturns;

    event AuctionEnded(address winner, uint highestBid);

    // Başarısızları açıklayan hatalar

    /// Fonksiyon erken çağırıldı.
    /// `time` de tekrar deneyin.
    error TooEarly(uint time);
    /// Fonksyion geç çağırıldı.
    /// `time` dan sonra çağırılamaz.
    error TooLate(uint time);
    /// auctionEnd fonksyionu zaten çağırıldı.
    error AuctionEndAlreadyCalled();

    // Modifierlar fonksiyon girdilerini kontrol etmenin
    // kolay bir yöntemidir. `onlyBefore` modifierı aşağıdaki
    // `bid` e uygulandı:
    // Yeni fonksyionun gövde kısmı modifierın gövde kısmı oluyor.
    // Sadece `_` eski fonksiyonun gövdesiyle değişiyor..
    modifier onlyBefore(uint time) {
        if (block.timestamp >= time) revert TooLate(time);
        _;
    }
    modifier onlyAfter(uint time) {
        if (block.timestamp <= time) revert TooEarly(time);
        _;
    }

    constructor(
        uint biddingTime,
        uint revealTime,
        address payable beneficiaryAddress
    ) {
        beneficiary = beneficiaryAddress;
        biddingEnd = block.timestamp + biddingTime;
        revealEnd = biddingEnd + revealTime;
    }
    /// `blindedBid` = keccak256(abi.encodePacked(value, fake, secret))
    /// ile gizli bir teklif ver. Gönderilen ether sadece teklif doğru
    /// bir şekilde açıklandıysa geri alınabilir. Teklif eğer "value"daki
    /// değer ile en az gönderilen Ether kadar ya da "fake" değeri `false`
    /// ise geçerlidir.  Bir miktar Ether yatırılması  gereksenede
    /// "fake" değerini `true` yapmak ve "value" değerinden
    /// farklı miktarda Ether göndermek gerçek teklifi gizlemenin yöntemleridir.
    /// Aynı adres birden fazla kez para yatırabilir.
    function bid(bytes32 blindedBid)
        external
        payable
        onlyBefore(biddingEnd)
    {
        bids[msg.sender].push(Bid({
            blindedBid: blindedBid,
            deposit: msg.value
        }));
    }

    /// Gizli teklifini açıkla. Tüm teklifler arasında en yüksek olan hariç
    /// doğru şekilde açıklanmış tüm tekliflerin parasını iade alabilirsin.
    function reveal(
        uint[] calldata values,
        bool[] calldata fakes,
        bytes32[] calldata secrets
    )
        external
        onlyAfter(biddingEnd)
        onlyBefore(revealEnd)
    {
        uint length = bids[msg.sender].length;
        require(values.length == length);
        require(fakes.length == length);
        require(secrets.length == length);

        uint refund;
        for (uint i = 0; i < length; i++) {
            Bid storage bidToCheck = bids[msg.sender][i];
            (uint value, bool fake, bytes32 secret) =
                    (values[i], fakes[i], secrets[i]);
            if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
                // Teklif açıklanmadı
                // Yatırılan parayı iade etme
                continue;
            }
            refund += bidToCheck.deposit;
            if (!fake && bidToCheck.deposit >= value) {
                if (placeBid(msg.sender, value))
                    refund -= value;
            }
            // Göndericinin gönderdiği parayı tekrar geri almasını
            // imkansız hale getir.
            bidToCheck.blindedBid = bytes32(0);
        }
        payable(msg.sender).transfer(refund);
    }

    /// Fazladan para yatırılmış bir teklifi geri çek.
    function withdraw() external {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // Bu değeri sıfıra eşitlemek önemli çünkü alıcı bu fonksiyonu
            // `send` tamamlanmadan tekrar çağırırsa (reentrancy) alması gerekenden
            // daha fazla para çekebilir. (yukarıdaki şartlar -> etkiler -> etkileşimler
            // hakkındaki bilgilendirmeye bakabilirsiniz)
            pendingReturns[msg.sender] = 0;

            payable(msg.sender).transfer(amount);
        }
    }

    /// İhaleyi bitir ve en yüsek teklifi
    /// hak sahibine gönder.
    function auctionEnd()
        external
        onlyAfter(revealEnd)
    {
        if (ended) revert AuctionEndAlreadyCalled();
        emit AuctionEnded(highestBidder, highestBid);
        ended = true;
        beneficiary.transfer(highestBid);
    }

    // "internal" (içsel) bir fonksiyon yani sadece kontratın
    // kendisi (ya da bu kontrattan çıkan (derive edilen) kontratlar)
    // bunu çağırabilir.
    function placeBid(address bidder, uint value) internal
            returns (bool success)
    {
        if (value <= highestBid) {
            return false;
        }
        if (highestBidder != address(0)) {
            // Önceki en yüksek teklifin parasını iade et.
            pendingReturns[highestBidder] += highestBid;
        }
        highestBid = value;
        highestBidder = bidder;
        return true;
    }
}

Güvenli Uzaktan Alışveriş

Uzaktan bir mal satın almak birden fazla tarafın birbirine güvenmesini gerektirir. En basit durumda bir satıcı bir de alıcı olur. Alıcı ürünü satıcıdan almak ister, satıcı da karılığında parayı (ya da eş değeri bir şeyi) almak. Burada problemli kısım kargolama: Kesin olarak malın alıcıya ulaştığından emin olmanın yolu yok.

Bu problemmi çözmenin birden fazla yolu var ama hepsinin bir şekilde bir eksiği oluyor. Aşağıdaki örnekte, iki taraf da kontrata malın değerinin iki katını yatırırlar. Yatırma gerçekleştiği anda alıcı onaylayana kadar iki tafaında parası içeride kitli kalır. Alıcı satın almayı onayladığında malın değeri (yatırdığının yarısı) karşı tarafa geçer ve satıcı malın üç katını (yatırdığı iki kat ve alıcının yatırdığı malın değeri) geri çeker. Bu sistemin arkaplanındaki fikir iki tarafında problemi çözmeleri için gönüllü olmaları yoksa ikisinin parası da içeride sonsuza kadar kitli kalacak

Bu kontrat tabi ki bu problemi çözmüyor ama makine benzeri yapıları sözleşmede nasıl kullanabileceğinize dair genel bir bakış sağlıyor.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Purchase {
    uint public value;
    address payable public seller;
    address payable public buyer;

    enum State { Created, Locked, Release, Inactive }
    // state değişkeni varsayılan olarak ilk üyedir,  `State.created`
    State public state;

    modifier condition(bool condition_) {
        require(condition_);
        _;
    }

    /// Bu fonksiyonu sadece alıcı çağırabilir
    error OnlyBuyer();
    /// BU fonksyionu sadece satıcı çağırabilir.
    error OnlySeller();
    /// Bu fonksiyon şu an çağırılamaz.
    error InvalidState();
    /// Girilen değer çift olmalı.
    error ValueNotEven();

    modifier onlyBuyer() {
        if (msg.sender != buyer)
            revert OnlyBuyer();
        _;
    }

    modifier onlySeller() {
        if (msg.sender != seller)
            revert OnlySeller();
        _;
    }

    modifier inState(State state_) {
        if (state != state_)
            revert InvalidState();
        _;
    }

    event Aborted();
    event PurchaseConfirmed();
    event ItemReceived();
    event SellerRefunded();

    // `msg.value` in çift olduğundan emin ol.
    // Eğer tek sayı ise bölme kırpılmış bir sonuç olacak.
    // Çarpma ile tek sayı olmadığını kontrol et.
    constructor() payable {
        seller = payable(msg.sender);
        value = msg.value / 2;
        if ((2 * value) != msg.value)
            revert ValueNotEven();
    }

    /// Satın almayı iptal et ve etheri geri al.
    /// Sadece satıcı tarafından kontrat kitlenmeden
    /// önce çağırılabilir.
    function abort()
        external
        onlySeller
        inState(State.Created)
    {
        emit Aborted();
        state = State.Inactive;
        // Burada transfer'i direkt olarak kullanıyoruz.
        // Tekrar giriş (reentrancy) saldırılarına karşı güvenli
        // çünkü fonksiyondaki son çağrı (call) ve durumu (state)
        // zaten değiştirdik.
        seller.transfer(address(this).balance);
    }

    /// Alıcı olarak satın almayı onayla.
    /// İşlem `2 * value` kadar ether içermeli.
    /// Ether confirmReceived fonksiyonu çağırılana
    /// kadar kitli kalacak.
    function confirmPurchase()
        external
        inState(State.Created)
        condition(msg.value == (2 * value))
        payable
    {
        emit PurchaseConfirmed();
        buyer = payable(msg.sender);
        state = State.Locked;
    }

    /// Malı teslim aldığını onayla (alıcı)
    /// Kitli etheri serbest bırakacak.
    function confirmReceived()
        external
        onlyBuyer
        inState(State.Locked)
    {
        emit ItemReceived();
        // Durumu (state) önceden değiştirmek oldukça önemli
        // yoksa aşağıdaki `send` i kontratlar burada tekrar
        // bu fonksiyonu çağırabilir. (tekrar giriş saldırısı - reentrancy attack)
        state = State.Release;

        buyer.transfer(value);
    }

    /// Bu fonksiyon satıcıya iade eder
    /// (satıcının kitli parasını geri öder)
    function refundSeller()
        external
        onlySeller
        inState(State.Release)
    {
        emit SellerRefunded();
        // Durumu (state) önceden değiştirmek oldukça önemli
        // yoksa aşağıdaki `send` i kontratlar burada tekrar
        // bu fonksiyonu çağırabilir. (tekrar giriş saldırısı - reentrancy attack)
        state = State.Inactive;

        seller.transfer(3 * value);
    }
}

Mikro Ödeme Kanalı

Bu bölümde bir ödeme kanalı örneğinin nasıl yapılacağını öğreneceğiz. Bu sistem belli kişiler arasındaki Ether transferini güvenli, anında ve işlem masrafsız gerçekleştirmek için kriptografik imzaları kullanacak. Bu örnek için, imzaların nasıl imzalandığını ve doğrulandığını anlamamız gerekiyor.

İmza Oluşturma ve Doğrulama

Mesela Alice Bob’a bir miktar Ether göndermek istiyor. Başka bir deyişle Alice gönderici Bob ise alıcı.

Alice’in Bob’a, çek yazmaya benzer bir şekilde, sadece kriptografik olarak imzalanmış off-chain (zincir dışı) bir mesaj göndermesi yeterli.

Alice ve Bob bu imzaları ödemeleri yetkilendirmek için kullanabilirler ve bu Ethereum üzerindeki akıllı kontratlar ile mümkün olan bir şey. Alice Ethere göndermek için basit bir akıllı kontrat yazacak ancak bu fonksiyonu kendi çağırmak yerine Bob’un çağırmasını isteyecek böylece Bob işlem masraflarını da ödemiş olacak.

Kontrat aşağıdaki şekilde ilerleyecek.

  1. Alice içine ödeme için gerekli Ether’i de ekleyerek ReceiverPays kontratını yayınlayacak.

  2. Alice ödemeyi gizli anahtarı ile imzalayarak yetkilendirecek.

  3. Alice kriptografik olarak imzalanmış mesajı Bob’a gönderecek. Mesajın gizli tutulmasına (daha sonra açıklanacak) gerek yok ve mesaj herhangi bir yöntem ile gönderilebilir.

  4. Bob imzalanmış mesajı akıllı kontrata girerek ödemesini alabilir, akullı kontrat mesajın gerçekliğini doğrular ve ödemeyi serbest bırakır.

İmza oluşturma

Alice’in ödemeyi imzlamak için Ethereum ağı ile etkileşime girmesine gerek yok, bu işlem tamamiyle çevrimdışı gerçekleştirilebilir. Bu eğitimde, mesajları tarayıcıda web3.js ve MetaMask kullanarak ve getirdiği güvenlik kazançları için EIP-712 gösterilen metot ile imzalayacağız.

/// En başta "Hash"leme işleri daha kolay bir hale getirir
var hash = web3.utils.sha3("imzalanacak mesaj");
web3.eth.personal.sign(hash, web3.eth.defaultAccount, function () { console.log("İmzalandı"); });

Not

web3.eth.personal.sign mesajın uzunluğunu imzalanmış bilginin başına ekler. İlk olarak “hash”lediğimiz için, mesaj her zaman 32 bayt uzunluğunda olacak ve dolayısıyla bu uzunluk ön eki her zaman aynı olacak.

Ne İmzalanacak

Ödeme gerçekleştiren bir kontrat için, imzalanmış bir mesaj aşağıdakiler içermeli:

  1. Alıcının adresi.

  2. Transfer edilecek miktar.

  3. Tekrarlama saldırılarına karşı önlem

Tekrarlama saldırısı, imzalanmış bir mesajın tekrar yetkilendirme için kullanılmasıdır. Tekrarlama saldırılarını önlemek için Ethereum işlemlerinden kullanan bir cüzdandan yapılan işlem sayısını, nonce, kullanan tekniği kullanacğız. Akıllı kontrat bir nonce un bir kaç kez kullanılıp kullanılmadığını kontrol edecek.

Başka bir tekrarlamma saldırısı açığı ödemeyi gönderen kişi ReceiverPays akıllı kontratını yayınlayıp sonrasında yok edip sonra tekrar yayınladığında oluşur. Bunun sebebi tekrar yayınlanan kontrat önceki kontratta kullanılan nonce ları bilemediğinden saldırgan eski mesajları tekrar kullanabilir.

Alice buna karşı korunmak için kontratın adresini de mesajın içerisine ekleyebilir. Böylece sadece kontrat’ın adresini içeren mesajlar onaylanır. Bu örneği bu bölümün sonundaki tam kontratın claimPayment() fonksiyonundaki ilk iki satırda görebilirsiniz.

Argümanları Paketleme

Şimdi imzalanmış mesajımızda nelerin olacağına karar verdiğimize göre mesajı mesajı oluşturup, hashleyip, imzalamaya hazırız. Basit olsun diye verileri art arda bağlayacağız. ethereumjs-abi kütüphanesi bize soliditySHA3 adında Solidity’deki abi.encodePacked ile enkode edilmiş argümanlara keccak256 fonksiyonu uygulanması ile aynı işlevi gören bir fonksiyon sağlıyor. Aşağıda ReceiverPays için düzgün bir imza sağlayan JavaScript fonksiyonunu görebilirsiniz.

// recipient alıcı adres,
// amount, wei cinsinden, ne kadar gönderilmesi gerektiği
// nonce, tekrarlama saldırılarını önlemek için eşsiz bir sayı
// contractAddress, kontratlar arası tekrarlama saldırısını engellemek için kontrat adresi
function signPayment(recipient, amount, nonce, contractAddress, callback) {
    var hash = "0x" + abi.soliditySHA3(
        ["address", "uint256", "uint256", "address"],
        [recipient, amount, nonce, contractAddress]
    ).toString("hex");

    web3.eth.personal.sign(hash, web3.eth.defaultAccount, callback);
}

Solidity’de İmzalayanı Bulma

Genelde ECDSA imzaları iki parametreden oluşur, r ve s. Ethereum’daki imzalar v denilen üçüncü bir parametre daha içerir. v parametresi ile mesajı imzalamak için kullanılmış cüzdanın gizli anahtarı doğrulanabiliyirsiniz. Solidity ecrecover fonksiyonunu gömülü olarak sağlamaktadır. Bu fonksiyon mesajla birlikte r, s ve v parametrelerini de alır ve mesajı imzalamak için kullanılmış adresi verir.

İmza Parametrelerini Çıkartma

web3.js ile oluşturulmuş imzalar r, s ve v’in birleştirilmesi ile oluşturulur, yani ilk adım bu parametreleri ayırmak. Bunu kullanıcı tarafında da yapabilirsiniz ancak parametre ayırma işleminin akıllı kontratın içinde olması akıllı kontrata üç parametre yerine sadece bir parametre göndermemizi sağlar. Bir bayt dizisini (byte array) bileşenlerine ayırmak biraz karışık dolayısıyla bu işlemi splitSignature fonksiyonunda yapmak için inline assembly kullanacağız. (Bu bölümün sonundaki tam kontrattaki üçüncü fonksiyon.)

Mesaj Hashini Hesaplama

Akıllı kontratın tam olarak hangi parametrelerin izalandığını bilmesi gerekiyor çünkü kontratın imzzayı doğrulamak için mesajı parametrelerinden tekrar oluşturması lazım. claimPayment fonksiyonundaki prefixed ve recoverSigner fonksiyonları bu işlemi gerçekleştiriyor.

Tam Kontrat

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ReceiverPays {
    address owner = msg.sender;

    mapping(uint256 => bool) usedNonces;

    constructor() payable {}

    function claimPayment(uint256 amount, uint256 nonce, bytes memory signature) external {
        require(!usedNonces[nonce]);
        usedNonces[nonce] = true;

        // istemcide imzalanmış mesajı tekrar oluşturur.
        bytes32 message = prefixed(keccak256(abi.encodePacked(msg.sender, amount, nonce, this)));

        require(recoverSigner(message, signature) == owner);

        payable(msg.sender).transfer(amount);
    }

    /// sözleşmeyi yok eder ve kalan parayı geri alır
    function shutdown() external {
        require(msg.sender == owner);
        selfdestruct(payable(msg.sender));
    }

    /// imza methodları
    function splitSignature(bytes memory sig)
        internal
        pure
        returns (uint8 v, bytes32 r, bytes32 s)
    {
        require(sig.length == 65);

        assembly {
            // uzunluk önekinden sonraki ilk 32 bayt.
            r := mload(add(sig, 32))
            // ikinci 32 bayt
            s := mload(add(sig, 64))
            // son bayt (gelecek 32 baytın son baytı)
            v := byte(0, mload(add(sig, 96)))
        }

        return (v, r, s);
    }

    function recoverSigner(bytes32 message, bytes memory sig)
        internal
        pure
        returns (address)
    {
        (uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);

        return ecrecover(message, v, r, s);
    }

    /// eth_sign'i kopyalayan önüne eklenmiş hash oluşturur.
    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }
}

Basit Bir Ödeme Kanalı Yazmak

Alice şimdi ödeme basit ama tam işlevsel bir ödeme kanalı oluşturacak. Ödeme kanalları anında ve masrafsız tekrarlayan Ether transferleri gerçekleştirmek için kriptografik imzaları kullanırlar.

Ödeme Kanalı Nedir?

Ödeme kanalları katılımcıların herhangi bir işlem gerçekleştirmeden tekrarlayan Ether transferleri gerçekleştirmelerini sağlar. Bu sayesede ödemeyle ilgili gecikme ve masraflardan kurtulabilirsiniz. Şimdi iki kişi (Alice ve Bob) arasında tek yönlü bir ödeme kanalı nasıl oluşturul onu göreceğiz. Böyle bir sistemi 3 adımda oluşturabiliriz. Bunlar:

  1. Alice ödeme kanalına Ether yükler böylece ödeme kanali “açık” hale gelir.

  2. Alice ne kadar Ether’in ödenmesi gerektiğini bir mesajda belirtir. Bu adım her ödemede tekrar gerçekleştirilir.

  3. Bob Ether ödemesini alıp kalanı geri göndererek ödeme kanalını kapatır.

Not

Sadece 1. ve 3. adımlar Ethereum işlemi gerektiriyor. 2. adımda gönderici kriptografik olarak imzalanmış mesajı alıcıya zincir dışı (off-chain) bir şekilde (mesela e-posta) gönderebilir. Kısaca herhangi bir sayıda transfer için 2 Ethereum işlemi gerekiyor.

Bob kesinlikle parasını alacak çünkü Ether bir akıllı kontratta tutuluyor ve geçerli bir imzalı mesaj ile akıllı kontratlar her zaman işlemi gerçekleştirir. Akıllı kontrat ayrıca zaman aşımını da zorunlu tutar, yani alıcı parası almazsa Alice eninde sonunda parasını geri alabilir. Zaman aşımının süresine katılımcılar kendi karar verir. İnternet kafedeki kullanım süresi gibi kısa süreli bir işlem için, ödeme kanalı süreli bir şekilde oluşturulabilir. Diğer bir yandan, bir çalışana saatlik maaşını ödemek gibi tekrarlayan bir ödeme için ödeme kanalı bir kaç ay ya da yıl açık kalabilir.

Ödeme Kanalını Açma

Ödeme kanalını açmak için Alice içine gerekli Ether’i ekleyip ve alıcının kim olduğunu girerek akıllı kontratı yayınlar. Bu işlemi bölümün sonundaki kontratta SimplePaymentChannel fonksiyonu gerçekleştirir.

Ödeme Gerçekleştirme

Alice ödemeyi Bob’a imzalanmış mesajı göndererek yapar. Bu adım tamammiyle Etherum ağının dışında gerçekeleşir. Mesaj gönderici tarafında kriptografik olarak imzalanır ve direkt olarak alıcıya gönderilir.

Her mesaj aşağıdaki bilgileri içerir:

  • Akıllı kontratın adresi, kontratlar arası tekrarlama saldırılarını önlemek için.

  • Alıcıya borçlu olunan Ether miktarı.

Ödeme kanalı bütün transferler gerçekleştikten sonra sadece bir kez kapanır. Bundan dolayı sadece bir mesajın ödemesi gerçekleşir. Bu yüzden her mesaj küçük ödemeler yerine toplam gönderilmesi gereken Ether miktarını içerir. Alıcı doğal olarak en yüksek miktarı alabilmek için en güncel mesajın ödemesini alır. Artık akıllı kontrat sadece bir mesaj okuduğunderstan artık işlem sayısını (nonce) mesaja eklemeye gerek yok ancak akıllı kontratın adresine mesajın başka bir ödeme kanalında kullanılmaması için hala ihtiyaç var.

Aşağıda önceki bölümdeki mesajın kriptografik imzalanmasını sağlayan JavaScript kodunun düzenlenmiş bir halini bulabilirsiniz.

function constructPaymentMessage(contractAddress, amount) {
    return abi.soliditySHA3(
        ["address", "uint256"],
        [contractAddress, amount]
    );
}

function signMessage(message, callback) {
    web3.eth.personal.sign(
        "0x" + message.toString("hex"),
        web3.eth.defaultAccount,
        callback
    );
}

// contractAddress, kontratlar arası tekrarlama saldırısını engellemek için kontrat adresi
// amount, wei cinsinden, ne kadar gönderilmesi gerektiği

function signPayment(contractAddress, amount, callback) {
    var message = constructPaymentMessage(contractAddress, amount);
    signMessage(message, callback);
}

Ödeme Kanalını Kapatma

Bob ödemesini almaya hazır olduğunda ödeme kanalını da close fonksiyonunu çağırarak kapatmanın vakti de gelmiş demektir. Kanal kapatıldığında alıcı kendine borçlu olunan Ether miktarını alır ve kalan miktarı Alice’e geri göndererek kontratı yok eder. Bob sadece Alice tarafında imzalanmış bir mesaj ile kanalı kapatabilir.

Akıllı kontratın göndericiden gelen geçerli bir mesajı doğrulaması gerekir. Bu doğrulama süreci alıcının kullandığı süreç ile aynıdır. Solidity fonksiyonlarından isValidSignature ve recoverSigner (ReceiverPays kontratından aldık) önceki bölümdeki JavaScript hallerindekiyle aynı şekilde çalışır.

Sadece ödeme kanalının alıcısı close fonksiyonunu çağırabilir. Alıcı da doğal olarak en yüksek miktarı taşığı için en güncel mesajı gönderir. Eğer gönderici bu mesajı çağırabiliyor olsaydı daha düşük bir miktar içeren bir mesaj ile çağırıp, ödemeleri gerekenden daha düşük bir para göndererek hile yapabilirlerdi.

Fonksiyon verilen parametreler ile imzalanmış mesajı doğrular. Eğer her şey uygunsa, alıcıya kendi payına düşen Ether miktarı gönderilir ve göndericiye kalan miktar selfdestruct ile gönderilir. Tam kontratta close fonksiyonunu görebilirsiniz.

Kanalın Zaman Aşımına Uğraması

Bob istediği zaman ödeme kanalını kapatabilir anca kapatmazsa Alice’in bir şekilde parasını geri alması gerekiyor. Bunun için kontrata bir zaman aşımı süresi girilir. Süre dolduğunda, Alice claimTimeout fonksiyonunu çağırarak içerideki parasını geri alabilir. claimTimeout fonksyionunu tam kontratta görebilirsiniz.

Bu fonksiyon çağırıldıktan sonra Bob artık sistemden Ether alamaz dolayısıyla Bob’un zaman aşımına uğramadan parasını alması oldukça önemli.

Tam Kontrat

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract SimplePaymentChannel {
    address payable public sender;      // göndericinin adresi.
    address payable public recipient;   // alıcının adresi.
    uint256 public expiration;  // kapanmaması durumunda zaman aşımı süresi.

    constructor (address payable recipientAddress, uint256 duration)
        payable
    {
        sender = payable(msg.sender);
        recipient = recipientAddress;
        expiration = block.timestamp + duration;
    }

    /// alıcı, göndericinin imzalı mesajı ile istediği zaman kanalı kapatabilir.
    /// alıcı alacaklısı olduğu miktarı alıp
    /// kalanı göndericiye geri gönderir.
    function close(uint256 amount, bytes memory signature) external {
        require(msg.sender == recipient);
        require(isValidSignature(amount, signature));

        recipient.transfer(amount);
        selfdestruct(sender);
    }

    /// gönderici zaman aşımı süresini istediği zaman arttırabilir
    function extend(uint256 newExpiration) external {
        require(msg.sender == sender);
        require(newExpiration > expiration);

        expiration = newExpiration;
    }

    /// Eğer süre alıcı kanalı kapatmadan dolarsa
    /// Ether göndericiye geri döner
    function claimTimeout() external {
        require(block.timestamp >= expiration);
        selfdestruct(sender);
    }

    function isValidSignature(uint256 amount, bytes memory signature)
        internal
        view
        returns (bool)
    {
        bytes32 message = prefixed(keccak256(abi.encodePacked(this, amount)));

        // imzanın göndericiden geldiğini kontrol et
        return recoverSigner(message, signature) == sender;
    }

    /// Aşağıdaki tüm konksyionlar 'imza oluşturma ve doğrulama'
    /// bölümünden alındı.

    function splitSignature(bytes memory sig)
        internal
        pure
        returns (uint8 v, bytes32 r, bytes32 s)
    {
        require(sig.length == 65);

        assembly {
            // uzunluk önekinden sonraki ilk 32 bayt.
            r := mload(add(sig, 32))
            // ikinci 32 bayt
            s := mload(add(sig, 64))
            // son bayt (gelecek 32 baytın son baytı)
            v := byte(0, mload(add(sig, 96)))
        }

        return (v, r, s);
    }

    function recoverSigner(bytes32 message, bytes memory sig)
        internal
        pure
        returns (address)
    {
        (uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);

        return ecrecover(message, v, r, s);
    }

    /// eth_sign'i kopyalayan önüne eklenmiş hash oluşturur.
    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
    }
}

Not

splitSignature fonksiyonu bütün güvenlik önlemlerini almıyor. Gerçek bir uygulamada openzeppelin’in versionu gibi daha iyi test edilmiş bir kütüphane kullanılmalı.

Ödemeleri Doğrulama

Önceki bölümlerdekinin aksine, ödeme kanalındaki mesajlar anında alınmamakta. Alıcı mesajların takibini yapıp zamanı geldiğinde ödeme kanalını kapatır. Yani bu durumda alıcının mesajları kendisinin doğrulaması oldukça önemli. Yoksa alıcının ödemesini kesin alacağının bir garantisi yok.

Alıcı her mesajı aşağıdaki işlemler ile doğrulamalı:

  1. Mesajdaki kontrat adresinin ödeme kanalı ile aynı olduğunu kontrol et

  2. Yeni toplam miktarın beklenen miktar ile aynı olduğunu kontrol et

  3. Yeni toplam miktarın kontrattakinden fazla olmadığını kontrol et

  4. Mesajın ödeme kanalının göndericisinden geldiğini kontrol et.

Bu doğrulamayı yazmak için ethereumjs-util kütüphanesini kullanacağız. Son adım için bir çok farklı yol var ve biz JavaScript kullanacağuz. Aşağıdaki kod constructPaymentMessage fonksiyonunu yukarıdaki imzalama JavaScript kodundan ödünç alıyor:

// Bu eth_sign JSON-RPC metodunun ön ekleme özelliğini taklit eder.
function prefixed(hash) {
    return ethereumjs.ABI.soliditySHA3(
        ["string", "bytes32"],
        ["\x19Ethereum Signed Message:\n32", hash]
    );
}

function recoverSigner(message, signature) {
    var split = ethereumjs.Util.fromRpcSig(signature);
    var publicKey = ethereumjs.Util.ecrecover(message, split.v, split.r, split.s);
    var signer = ethereumjs.Util.pubToAddress(publicKey).toString("hex");
    return signer;
}

function isValidSignature(contractAddress, amount, signature, expectedSigner) {
    var message = prefixed(constructPaymentMessage(contractAddress, amount));
    var signer = recoverSigner(message, signature);
    return signer.toLowerCase() ==
        ethereumjs.Util.stripHexPrefix(expectedSigner).toLowerCase();
}

Modüler Kontratlar

Kontratları oluştururken modüler bir yaklaşım izlemek kodların karışıklığını azaltıp, okunabilirliğini arttırır. Bu durumda hataların ve açıkların daha kolay bir şekilde bulunmasını sağlar. Eğer her modülün nasıl davranacağını izole bir şekilde tanımlar ve kontrol ederseniz, sadece bütün kontratta olup biten yerine o kontratlar arasındaki ilişkileri inceleyebilirsiniz. Aşağıdaki örnekte kontrat adresler arasında gönderilenin beklenen şekilde olup olmadığını görmek için Balances library kütüphanesinin move metodunu kullanır. Balances kütüphanesinin asla nefatif bir bakiye çıkarmadığı ya da bütün bakiyelerin toplamından overflow yaratmayacağı kolaylıkla doğrulanabilir ve bu durum kontratın yaşam süresi boyunca değişmez.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

library Balances {
    function move(mapping(address => uint256) storage balances, address from, address to, uint amount) internal {
        require(balances[from] >= amount);
        require(balances[to] + amount >= balances[to]);
        balances[from] -= amount;
        balances[to] += amount;
    }
}

contract Token {
    mapping(address => uint256) balances;
    using Balances for *;
    mapping(address => mapping (address => uint256)) allowed;

    event Transfer(address from, address to, uint amount);
    event Approval(address owner, address spender, uint amount);

    function transfer(address to, uint amount) external returns (bool success) {
        balances.move(msg.sender, to, amount);
        emit Transfer(msg.sender, to, amount);
        return true;

    }

    function transferFrom(address from, address to, uint amount) external returns (bool success) {
        require(allowed[from][msg.sender] >= amount);
        allowed[from][msg.sender] -= amount;
        balances.move(from, to, amount);
        emit Transfer(from, to, amount);
        return true;
    }

    function approve(address spender, uint tokens) external returns (bool success) {
        require(allowed[msg.sender][spender] == 0, "");
        allowed[msg.sender][spender] = tokens;
        emit Approval(msg.sender, spender, tokens);
        return true;
    }

    function balanceOf(address tokenOwner) external view returns (uint balance) {
        return balances[tokenOwner];
    }
}