1. Anasayfa
  2. Solidity

Solidity Eğitimi Depolama Hakkında Her Şey

Solidity Veri Konumları Hakkında Her Şey — Depolama

Solidity Eğitimi Depolama Hakkında Her Şey
Solidity Eğitimi Depolama
0

Solidity Eğitimi yazı dizisi kapsmaında Depolama referanslarına ve akıllı sözleşme depolama düzenine dalıyoruz.

Bu serinin Solidity Eğitimi: Veri Konumları Hakkında Her Şey başlıklı içeriğinin ilk parçası Solidity Eğitimi : Bellek Hakkında Her Şey idi. Solidity Eğitimi : Depolama Hakkında Her Şey, Veri Konumları Hakkında ikinci altbaşlık oldu…

Bu haftanın Solidity makalesinde, EVM’deki önemli bir veri konumunu daha ayrıntılı olarak ele alıyoruz: akıllı sözleşmeli depolama.

Sözleşme depolama düzeninin nasıl çalıştığını, storage referanslarını göreceğiz. Ayrıca, yol boyunca bu popüler sözleşmelerin ve protokollerin arkasındaki Solidity kodunu öğrenirken, storage referanslarının pratikte nasıl çalıştığını öğrenmek için OpenZeppelin ve Compound‘dan bazı sözleşmeleri kullanacağız.

Solidity Eğitimi Depolama

  • Depolama Düzeni
  • Depolamanın Temelleri
  • Depolama ile Etkileşim
  • İşlev parametrelerinde depolama işaretçileri
  • İşlev gövdesindeki depolama işaretçileri
  • Depolama okuma maliyeti.
  • Sonuç Bağlamı : Depolama

Ethereum ve EVM tabanlı zincirdeki depolama modelini anlamak, iyi akıllı sözleşme geliştirme için çok önemlidir.

Verileri bir akıllı sözleşmede kalıcı olarak saklayabileceğiniz ve daha sonra gelecekteki uygulamalar için erişilebilecek tek yer onun deposudur. Her akıllı sözleşme, durumunu kendi kalıcı deposunda korur. “Akıllı sözleşme mini bir veri tabanı” gibi davranır, ancak diğer veritabanlarından farklı olarak bu veri tabanı herkese açık olarak erişilebilirdir. Akıllı sözleşme deposunda saklanan tüm değerler, blok zincirine bir işlem göndermeye gerek kalmadan ücretsiz olarak (statik çağrılar yoluyla) harici olarak okunabilir.

Ancak depoya yazmak oldukça pahalıdır. Aslında gaz maliyeti söz konusu olduğunda EVM’deki en pahalı işlemdir. Depolamanın içeriği sendTransaction çağrıları ile değiştirilebilir. Bu tür çağrılar durumu değiştirir. Sözleşme düzeyindeki değişkenlerin durum değişkenleri olarak adlandırılmasının nedeni budur.

Hatırlanması gereken önemli bir şey, Ethereum ve EVM’deki tasarım gereği, bir sözleşmenin kendisinin dışında herhangi bir depolamayı ne okuyabilir ne de yazamaz. A sözleşmesinin başka bir B sözleşmesinin deposundan okuyabilmesi veya yazabilmesinin tek yolu, B sözleşmesinin bunu yapmasını sağlayan işlevleri ortaya çıkarmasıdır.

Depolamanın Temelleri

Akıllı sözleşmenin depolanması, kalıcı bir okuma-yazma veri konumudur. Yani, sözleşme deposuna bir işlemde veri yazılırsa, işlem tamamlandıktan sonra devam eder. Bu işlemden sonra sözleşme deposunu okumak, bu önceki işlem tarafından yazılan/güncellenen verileri alacaktır.

Her sözleşmenin, aşağıdaki kurallara bağlı olarak tanımlanabilen kendi deposu vardır:

  • Durum değişkenlerini tut
  • İşlemler ve işlev çağrıları arasında kalıcı
  • Okumak bedava ama yazmak pahalı
  • Sözleşme depolama, sözleşme inşaatı sırasında önceden tahsis edilir.

Depoda bulunan değişkenler, Solidity‘de durum değişkenleri olarak adlandırılır.

Sözleşme depolama hakkında hatırlamanız gereken tek şey:

Depolama uzun vadeli ve pahalıdır!

Verileri depolamaya kaydetmek, EVM’de en yüksek miktarda gaz gerektiren işlemlerden biridir.

Depolamaya yazmanın gerçek maliyeti nedir?

Maliyet her zaman aynı değildir ve depolamaya yazma gazını hesaplamak, özellikle en son Ethereum 2.0 yükseltmesinden bu yana oldukça karmaşık bir formüldür.

Basit bir özet olarak, depolamaya yazmanın maliyeti aşağıdaki gibidir:

  • Bir depolama yuvasını başlatmak (ilk kez veya yuva herhangi bir değer içermiyorsa) sıfırdan sıfır olmayan bir değere 20.000 gaz maliyeti
  • Değeri bir depolama yuvasında düzenlemek 5.000 gaz maliyeti
  • Depolama yuvasındaki değeri silmek, 15.000 gazın geri ödenmesini sağlar.

Sözleşme depolamasını okumak gerçekten ücretsiz mi?

Akıllı sözleşmenin depolanması, harici olarak (bir EOA’dan) okumak için ücretsizdir. Böyle bir durumda, gaz ödenmesi gerekmez.

Bununla birlikte, okuma işlemi, sözleşmedeki, başka bir sözleşmedeki veya blok zincirindeki durumu değiştiren bir işlemin parçasıysa, gazın ödenmesi gerekir.

Bir sözleşme diğer sözleşmelerin depolamasını okuyabilir mi?

Varsayılan olarak, bir akıllı yürütme ortamı sırasında yalnızca kendi deposunda (daha sonra SLOAD aracılığıyla yapacağız) okuyabilir. Ancak, bu tür sözleşmeler ortak arabirimlerinde (ABI) belirli durum değişkenlerinden veya depolama yuvalarından veri okumayı sağlayan işlevleri ortaya çıkarırsa, akıllı bir sözleşme diğer akıllı sözleşmelerin depolanmasını da okuyabilir.

Depolama Düzeni

OpenZeppelin tarafından EVM’nin derinlemesine anlatıldığı bölüm 2 makalesinde açıklandığı gibi, akıllı bir sözleşmenin depolanması, kelime ile adreslenebilir bir alandır. Bu, ofsetler (bayt dizisindeki dizinler) aracılığıyla verilere eriştiğiniz doğrusal veri konumları (büyüyen bayt dizileri) olan bellek veya çağrı verilerinin tersidir.

Aksine, akıllı sözleşme deposu, anahtarın depodaki bir yuva numarasına karşılık geldiği ve değerin bu depolama yuvasında depolanan gerçek değer olduğu bir anahtar-değer eşlemesidir, kısaca veritabanı.

Akıllı bir sözleşmenin depolanması, aşağıdaki durumlarda yuvalardan oluşur:

  • Her depolama yuvası, 32 bayta kadar uzun sözcükler içerebilir.
  • Depolama yuvaları 0 konumunda başlar (dizi dizinleri gibi)
  • Toplamda 2²⁵⁶ depolama yuvası vardır (okuma/yazma için)

Özetle:

Akıllı bir sözleşmenin deposu, her bir yuvanın 32 bayta kadar boyut değerleri içerebildiği 2²⁵⁶ yuvadan oluşur.

İç işleyişte, sözleşme deposu, 256 bitlik anahtarların 256 bitlik değerlerle eşleştiği bir anahtar-değer deposudur. Her bir depolama yuvasındaki tüm değerler başlangıçta sıfıra ayarlanır, ancak sözleşme dağıtımı sırasında sıfır olmayan veya constructor‘ın belirli bir değerine de başlatılabilir.

Bir hangarda raf olarak sözleşmeli depolama

Steve Marx, makalesinde akıllı bir sözleşmenin depolanmasını “başlangıçta sıfırla dolu, dizideki girişlerin (dizinlerin) sözleşmenin depolama yuvalarını oluşturduğu astronomik olarak büyük bir dizi” olarak tanımlar.

Bu gerçek dünyada nasıl görünürdü? Akıllı bir sözleşmenin depolanmasını, muhtemelen en aşina olduğumuz bir şeyle nasıl temsil edebiliriz?

Bir sözleşmenin deposunun düzeni, bir inşaat malzemeleri satıcısının hangarına oldukça benzer.

Akıllı bir sözleşmenin saklanmasının nasıl göründüğüne dair iyi bir temsil elde etmek isterseniz, bir yapı markete uğrayın. Özellikle dış mekan kısmına (kamyonların ve kamyonetlerin çimento torbaları, tuğla paletler veya çelik raylar almaya gittiği yerlere) giderseniz, oldukça fazla aktivite fark edeceksiniz. Forkliftler her yerde ve gerçekten hızlı bir şekilde raflardan bir şeyler çıkarıyorlar. Bu, bir durum değişkenini okurken EVM’nin yaptığına eşdeğerdir:

contract Owner {
    
    address _owner;
    function owner() public returns (address) {
        return _owner;
    }
}

Yukarıdaki sözleşmede sadece bir raf (slot) bulunmaktadır. EVM, değişkeni “raf 0”dan yükler ve size sunmak için (yığın üzerine) boşaltır.

Durum değişkenlerinin düzeni

Lider Solidity geliştiricisi chriseth, bir sözleşmenin saklanmasını şu şekilde açıklar:

“Depolamayı sanal bir yapıya sahip geniş bir dizi olarak düşünebilirsiniz… çalışma zamanında değiştiremeyeceğiniz bir yapı – sözleşmenizdeki durum değişkenleri tarafından belirlenir”.

Yukarıdaki örnekten, Solidity‘nin sözleşmenizin tanımlı her durum değişkenine bir depolama yuvası atadığını görebiliriz. Statik olarak boyutlandırılmış durum değişkenleri için, depolama yuvaları, durum değişkenlerinin tanımlandığı sıraya göre yuva 0’dan başlayarak sürekli olarak atanır (kurallar farklıdır).

Chriseth‘in burada kastettiği, “işlev çağrılarında depolama oluşturulamaz“. Aslında, eğer kalıcı olması gerekiyorsa, bir fonksiyon aracılığıyla yeni depolama yuvalarında yeni depolama değişkenleri yaratmak pek mantıklı olmayacaktır (ancak mapping durumu biraz farklıdır).

Akıllı sözleşmenin depolanması, sözleşmenin inşası sırasında (sözleşmenin uygulandığı sırada) düzenlenir. Bu, sözleşmenin depo düzeninin sözleşmenin oluşturulması sırasında taşa konulduğu anlamına gelir. Düzen, sözleşme düzeyindeki değişken bildirimlerinize dayalı olarak “şekillidir” ve bu tür düzen, gelecekteki yöntem çağrılarıyla değiştirilemez.

solc komut satırı aracını kullanarak önceki sözleşmenin gerçek depolama düzenini görelim. Aşağıdaki komutu çalıştırırsanız:

solc contracts/Owner.sol --storage-layout --pretty-json

Aşağıdaki JSON çıktısını alacaksınız:

======= contracts/Owner.sol:Owner =======
Contract Storage Layout:
{
  "storage":
  [
    {
      "astId": 3,
      "contract": "contracts/Owner.sol:Owner",
      "label": "_owner",
      "offset": 0,
      "slot": "0",
      "type": "t_address"
    }
  ],
  "types":
  {
    "t_address":
    {
      "encoding": "inplace",
      "label": "address",
      "numberOfBytes": "20"
    }
  }
}

Yukarıdaki JSON çıktısından, bir dizi nesne içeren bir storage alanı görebiliriz. Bu dizideki her nesne bir durum değişkeni adına başvurur. Ayrıca her değişkenin bir slot ile eşlendiğini ve temel bir type‘a sahip olduğunu görebiliriz.

Bu, _owner değişkeninin aynı türden herhangi bir geçerli değerle değiştirilebileceği anlamına gelir (bizim durumumuzda address). Ancak, 0 yuvası bu değişken için ayrılmıştır ve her zaman orada olacaktır.

Şimdi durum değişkenlerinin depolamada nasıl düzenlendiğine bir göz atalım (daha fazla anlamak için Solidity belgelerine bakın).

Aşağıdaki Solidity kodunu göz önünde bulundurun:

pragma solidity ^0.8.0;
contract StorageContract {
    
    uint256 a = 10;
    uint256 b = 20;
}

Statik olarak boyutlandırılmış tüm değişkenler, tanımlandıkları sıraya göre sırayla depolama yuvalarına yerleştirilir.

Unutmayın: Depodaki her yuva 32 bayta kadar uzun değerler tutabilir.

Yukarıdaki örneğimizde, a ve b 32 bayt uzunluğundadır (türleri uin256 olduğundan). Bu nedenle, kendi depolama yuvalarına atanırlar.

Durum değişkenlerini bir depolama yuvasında paketleme.

Önceki örneğimizde istisnai bir şey yok. Ancak şimdi farklı boyutlarda birkaç uint değişkeninizin olduğu senaryoyu aşağıdaki gibi ele alalım:

pragma solidity ^0.8.0;
contract StorageContract {

    uint256 a = 10;
    uint64 b = 20;
    uint64 c = 30;
    uint128 d = 40;
    function readStorageSlot0() public view returns (bytes32 result) {
        assembly {
            result := sload(0)
        }
    }
    function readStorageSlot1() public view returns (bytes32 result) {
        assembly {
            result := sload(1)
        }
    
    }
}

Kontrat depolama slotlarını düşük seviyede okumak için iki temel fonksiyon yazdık. Çıktılara baktığımızda aşağıdakileri elde ederiz:

readStorage0
0: bytes32 result: 0x00..00....00...00a

readStorage1
0: bytes32 result: 0x00..28....1e...014

Solidity belgeleri şunları belirtir:

“32 bayttan daha azına ihtiyaç duyan birden fazla, bitişik öğe, mümkünse tek bir depolama yuvasında paketlenir…

Bir depolama yuvasındaki ilk öğe, alt sıraya göre hizalanmış olarak depolanır.”

Bu nedenle, değişkenler 32 bayttan küçük olduğunda, Solidity, sığabiliyorsa birden fazla değişkeni bir depolama yuvasına paketlemeye çalışır. Sonuç olarak, bir depolama yuvası birden fazla durum değişkenini tutabilir.

Bir temel tip, bir depolama yuvasında kalan boş alana sığmazsa, bir sonraki depolama yuvasına taşınır. Aşağıdaki Solidity sözleşmesi için:

pragma solidity ^0.8.0;
contract StorageContract {

    uint256 a = 10;
    uint64 b = 20;
    uint128 c = 30;
    uint128 d = 40;

}

Depolama düzeni şöyle görünür:

readStorage0
0: bytes32 result: 0x00..00....00...00a

readStorage1
0: bytes32 result: 0x00..28....1e...014

readStorage2
0: bytes32 result: 0x00..00....00...028

Daha somut bir örneğe, popüler bir Defi protokolüne bakalım: Aave.

Örnek: Aave Pool.sol sözleşmeleri.

Aave protokolü, likiditeyi yönetmek için ana akıllı sözleşmeler olarak Havuzları kullanır. Bunlar ana “kullanıcıya yönelik sözleşmelerdir”. Kullanıcılar, likidite sağlamak veya ödünç almak için (ya Solidity‘deki diğer sözleşmelerden veya web3/eter kitaplıklarını kullanarak) Aave havuzları sözleşmeleriyle doğrudan etkileşime girer.

Pool.sol‘de tanımlanan ana Aave Pool Sözleşmesi, bu makalenin konusuyla ilgili ilginç bir ada sahip bir sözleşmeyi devralır: PoolStorage.

/**
 * @title Pool contract
 * @author Aave
 * @notice Bir Aave protokolünün pazarıyla ana etkileşim noktası
 * - Users can:
 *   # Supply
 *   # Withdraw
 *   # Borrow
 *   # Repay
 *   # Swap kredileri değişken ve istikrarlı oran arasında
 *   # Enable/disable Teminat olarak sağlanan varlıkları yeniden dengeleme istikrarlı oranlı borçlanma pozisyonları
 *   # Liquidate positions
 *   # Execute Flash Loans
 * @dev Belirli bir pazarın PoolAddressesProvider'ına ait olan bir vekil sözleşmesi kapsamında olmak
 * @dev Tüm yönetici işlevleri, aynı zamanda aşağıdaki tabloda da tanımlanan PoolConfigurator sözleşmesi tarafından çağrılabilir.
 *   PoolAddressesProvider
 **/
contract Pool is VersionedInitializable, PoolStorage, IPool {
  using ReserveLogic for DataTypes.ReserveData;

  uint256 public constant POOL_REVISION = 0x1;
  IPoolAddressesProvider public immutable ADDRESSES_PROVIDER;

Protokolün Aave v3’ün Natspec yorumunda açıklandığı gibi, PoolStorage sözleşmesi bir amaca hizmet eder: “Havuz sözleşmesinin depolama düzenini tanımlar”.

PoolStorage sözleşmesinin Solidity koduna bakarsak, türlerinden dolayı bazı durum değişkenlerinin aynı depolama yuvasına paketlendiğini görebiliriz:

Yeşil Taralı Alan: flaş kredilerle ilgili durum değişkenlerinin (_flashLoanPremiumTotal ve _flashLoanPremiumToProtocol) her ikisi de uint128’dir. Birlikte paketlendiklerinde bütün bir depolama yuvasını kaplarlar (yuva nb 6).

Mavi taralı Alan: son iki durum değişkeni _maxStableRateBorrowSizePercent ve _flashLoanPremiumToProtocol uint64 ve uint16 türündedir. Ayrıca her ikisi de depolama yuvasında (yuva nb 7) paketlenir ve depolama yuvasında birlikte 10 bayt yer kaplar. Bu, potansiyel diğer durum değişkenlerinin kendileriyle paketlenmesi için biraz boşluk bırakır (kalan 22 bayt).

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.10;

import {UserConfiguration} from '../libraries/configuration/UserConfiguration.sol';
import {ReserveConfiguration} from '../libraries/configuration/ReserveConfiguration.sol';
import {ReserveLogic} from '../libraries/logic/ReserveLogic.sol';
import {DataTypes} from '../libraries/types/DataTypes.sol';

/**
 * @title PoolStorage
 * @author Aave
 * @notice Contract used as storage of the Pool contract.
 * @dev It defines the storage layout of the Pool contract.
 */
contract PoolStorage {
  using ReserveLogic for DataTypes.ReserveData;
  using ReserveConfiguration for DataTypes.ReserveConfigurationMap;
  using UserConfiguration for DataTypes.UserConfigurationMap;

  // Map of reserves and their data (underlyingAssetOfReserve => reserveData)
  mapping(address => DataTypes.ReserveData) internal _reserves;

  // Map of users address and their configuration data (userAddress => userConfiguration)
  mapping(address => DataTypes.UserConfigurationMap) internal _usersConfig;

  // List of reserves as a map (reserveId => reserve).
  // It is structured as a mapping for gas savings reasons, using the reserve id as index
  mapping(uint256 => address) internal _reservesList;

  // List of eMode categories as a map (eModeCategoryId => eModeCategory).
  // It is structured as a mapping for gas savings reasons, using the eModeCategoryId as index
  mapping(uint8 => DataTypes.EModeCategory) internal _eModeCategories;

  // Map of users address and their eMode category (userAddress => eModeCategoryId)
  mapping(address => uint8) internal _usersEModeCategory;

  // Fee of the protocol bridge, expressed in bps
  uint256 internal _bridgeProtocolFee;

  // Total FlashLoan Premium, expressed in bps
  uint128 internal _flashLoanPremiumTotal;

  // FlashLoan premium paid to protocol treasury, expressed in bps
  uint128 internal _flashLoanPremiumToProtocol;

  // Available liquidity that can be borrowed at once at stable rate, expressed in bps
  uint64 internal _maxStableRateBorrowSizePercent;

  // Maximum number of active reserves there have been in the protocol. It is the upper bound of the reserves list
  uint16 internal _reservesCount;
}

Devralma ile Depolama Düzeni

Sözleşmeli depolama düzeni de kalıtımı temel alır. Bir sözleşme diğer sözleşmelerden devralırsa, depolama düzeni devralma sırasını takip eder.

  • en temel sözleşmede tanımlanan durum değişkenleri 0 yuvasından başlar.
  • Aşağıdaki türetilmiş sözleşmede tanımlanan durum değişkenleri, alt sıralı yuvalara yerleştirilir (yuva 1, 2, 3, vb…).

Ayrıca, bir depolama yuvasındaki paketleme durumu değişkenleriyle aynı kuralların geçerli olduğunu unutmayın. Mümkünse, devralma yoluyla, farklı üst ve alt sözleşmelerdeki durum değişkenleri aynı depolama yuvasını paylaşır.

Depolama ile Etkileşim

EVM, depolama ile etkileşim kurmak için iki işlem kodu sağlar: Okumak için SLOAD ve depolamaya yazmak için SSTORE. Bu işlem kodlarının her ikisi de yalnızca satır içi montajda kullanılabilir. Solidity, derlemeden sonra başlık altındaki bu opcode’lara yazmayı durum değişkenine dönüştürür.

Depolamadan okuma

EVM, SLOAD işlem kodunu kullanarak bir akıllı sözleşmenin depolanmasını okuyabilir. SLOAD, depodan yığına bir sözcük yükler.

SLOAD işlem kodu, satır içi montajda mevcuttur. Belirli bir depolama yuvasında saklanan tüm kelime değerini kolayca almak için kullanılabilir.

function readStorageNb(uint256 slotNb) 
    public 
    view 
    returns (bytes32 result) 
{
    assembly {
        result := sload(slotNb)
    }
}

Solidity bunu işleyiş olarak gerçekleştirir. Bir alıcı işlevi aracılığıyla durum değişkenlerini okurken, otomatik olarak SLOAD işlem kodunu kullanacaktır. Örneğin, ERC20’deki popüler name() veya symbol() işlevleri. Bu işlevler, durum değişkenini döndürmekten başka bir şey yapmaz. OpenZeppelin’den aşağıdaki ekran görüntüsüne bakın.

*/
contract ERC20 is Context, IERC20, IERC20Metadata {
    mapping(address => uint256) private _balances;

    mapping(address => mapping(address => uint256)) private _allowances;

    uint256 private _totalSupply;

    string private _name;
    string private _symbol;

    /**
     * @dev Sets the values for {name} and {symbol}.
     *
     * The default value of {decimals} is 18. To select a different value for
     * {decimals} you should overload it.
     *
     * All two of these values are immutable: they can only be set once during
     * construction.
     */
    constructor(string memory name_, string memory symbol_) {
        _name = name_;
        _symbol = symbol_;
    }

    /**
     * @dev Returns the name of the token.
     */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
     * @dev Returns the symbol of the token, usually a shorter version of the
     * name.
     */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

Remix‘te name() işlevini sorgular ve alıcının hatalarını ayıklarsanız, aşağıdaki işlem kodlarını elde edersiniz.

; name()
JUMPDEST
PUSH1 60
PUSH1 03     ; step 1 - push the number 3 on the stack (= slot nb 3)
DUP1
SLOAD        ; step 2 - pass the number 3 as argument to SLOAD to
             ; load the value stored in the storage slot nb 3 
             ; (where the `_name` variable is stored)
; rest of the opcodes are emitted for brevity

Depolamaya yazma

EVM, SSTORE işlem kodunu kullanarak bir akıllı sözleşmenin deposuna yazabilir. SSTORE, bir kelimeyi depoya kaydeder.

Satır içi derleme kullanarak, bu şöyle görünür:

function writeToStorageSlot(uint256 slotNb) public {
    string memory value = "All About Solidity";    
    assembly {
        sstore(slotNb, value)
    }
}

OpenZeppelin‘den önceki ERC20 token örneğimizle devam edelim. ERC20 belirteç sözleşmesini dağıtırsak ve Remix kullanarak constructor‘da hata ayıklarsak aşağıdaki işlem kodlarını elde ederiz.

MLOAD  ; 1. load the token name from memory
PUSH1 ff
NOT
AND
DUP4
DUP1
ADD
OR
DUP6   ; 2. put back 3 (= slot nb for `name`) on top of the stack 
SSTORE ; 3. store at storage slot 3 the token `name` parameter
PUSH3 0003ee
JUMP  

Remix’te deneyin ve ERC20 belirtecini dağıttıktan sonra işlemin hatalarını ayıklayın.

Geth istemcisinin kaynak kodundan, SSTORE‘un yığından iki değer çıkardığını görebiliriz; en üstteki ilk konum, depolama konumu ve en üstteki ikinci val, depodaki değer deposudur.

Ayrıca interpreter.evm.StateDB.SetState(…) aracılığıyla sözleşme deposuna yazıldığında her iki değerin de yığından alınan her iki öğeyi de bytes32 değerlerine dönüştürdüğünü görebiliriz.

Bu nedenle, Depolama Düzeni bölümünde açıkladığımız şeyi doğrudan geth istemcisinin kaynak kodundan görebiliriz: akıllı sözleşme depolaması bytes32 anahtarını bytes32 değerlerine eşler ve bu nedenle her şey EVM tarafından işleyişte bytes32 kelimeleri olarak değerlendirilir.

SSTORE işlem kodunun akışını ayrıntılı olarak açıklayan diyagrama bakalım.

SSTORE’a üst düzey genel bakış

İşlev parametrelerinde depolama işaretçileri

storage anahtar sözcüğü, işlevlere parametre olarak geçirilen karmaşık değişkeni için geçirilebilir. Ama bu nasıl çalışıyor?

Bir işlev parametresinde storage belirtildiğinde, bu, işleve iletilen argümanın bir durum değişkeni olması gerektiği anlamına gelir.

Hala OpenZeppelin kütüphanesinde devam eden çok basit bir örnek kullanalım. Bu aynı zamanda, paketlerinin bir parçası olan sözleşmeleri ve kitaplıkları daha iyi anlamamıza da yardımcı olacaktır.

OpenZeppelin, Solidity sözleşmelerinde zamanlayıcılar ve zaman noktaları oluşturmak ve bunlarla uğraşmak için kullanılabilecek bir Timers kitaplığı sağlar. Aşağıdaki setDeadline(…) ve reset(…) işlevlerine ve bunların parametrelerine bakın.

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Timers.sol)

pragma solidity ^0.8.0;

/**
 * @dev Tooling for timepoints, timers and delays
 */
library Timers {
    struct Timestamp {
        uint64 _deadline;
    }

    function getDeadline(Timestamp memory timer) internal pure returns (uint64) {
        return timer._deadline;
    }

    function setDeadline(Timestamp storage timer, uint64 timestamp) internal {
        timer._deadline = timestamp;
    }

    function reset(Timestamp storage timer) internal {
        timer._deadline = 0;
    }

Bu iki işlev yalnızca depolama işaretçilerini kabul eder. Bu ne anlama geliyor?

Anlamak için bir TimeWatch sözleşmesi oluşturalım!

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import "@openzeppelin/contracts/utils/Timers.sol";

contract TimeWatch {
    using Timers for *;
    
    function startTimer(uint64 _deadline) public {
        Timers.Timestamp memory timer = Timers.Timestamp(0);
        timer.setDeadline(_deadline);
    }

}

Bu sözleşmeyi Remix’te derlemeyi denerseniz, Solidity derleyicisi aşağıdaki hatadan şikayet etmelidir:

TimeWatch Hata Ekran Görüntüsü
TimeWatch Hata Ekran Görüntüsü

Bu hata mantıklı. Zamanlayıcılar kitaplığındaki setDeadline(…) işlevi yalnızca depolama işaretçilerini kabul eder. Bu, işlevin argüman olarak kabul edeceği anlamına gelir:

  • ya durum değişkeni doğrudan
  • veya bir durum değişkenine referans (başka bir storage referansı veya depolama işaretçileri olarak adlandırmayı sevdiğim şey).

Ardından, çalışması için TimeWatch‘imizi yeniden yazalım. Çalışması için bir sıfırlama düğmesi de ekleyebiliriz.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "./Timers.sol";

contract TimeWatch {

    Timers.Timestamp timer;
    
    function startTimer(uint64 _deadline) public {
        Timers.setDeadline(timer, _deadline);
    }

}

İşlev parametreleri için temel bir depolama işaretçisi örneğini gördük. İşlev parametrelerindeki depolama işaretçilerini daha iyi anlamak için daha karmaşık bir örnekle biraz daha derine inelim.

Bir fonksiyonun parametresi bir storage referansı olduğunda, fonksiyon ya doğrudan bir durum değişkenini ya da bir durum değişkenine bir referansı kabul edebilir.

TimeWatch örneğimizi geliştirmeye devam edelim. Bir Yarış Turnuvası sözleşmesi oluşturmak için Timers kitaplığını kullanabiliriz. Bir sözleşme kullanmak, yarış organizatörüne veya zamanlayıcıları ve kuralları aldatma konusunda potansiyel olarak güvenilmeyen herhangi bir üçüncü tarafa duyulan güven düzeyini azaltacaktır.

Aşağıda bir prototip var. Sözleşme, ilgili yarışçıları ve zamanlarını haritalama yoluyla takip eder. Aşağıdaki startRacerTime(…) işlevine dikkat edin.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "./Timers.sol";

contract RaceTournament {

    mapping(address => Timers.Timestamp) racers;
    
    function startRacerTimer(address _racer, uint64 _deadline) public {
        Timers.Timestamp storage racerTimer = racers[_racer];
        Timers.setDeadline(racerTimer, _deadline);
    }

}

Bu iyi derlenir çünkü racerTimer, racers (sözleşme deposu) eşlemesinde bazı girişlere işaret eder. Bu nedenle, bu değişken sözleşme depolamasına bir başvuru olduğundan, Timers kitaplığındaki setDeadline(…) işlevi onu geçerli bir işlev argümanı olarak kabul edecektir.

İşlev gövdesindeki depolama işaretçileri

Yerel bir değişkene (işlev gövdesinde tanımlanan) bir depolama değişkeni atamak, değişken temel türde olduğunda her zaman kopyalar.

Ancak, karmaşık veya dinamik türler için kurallar farklıdır. storage anahtar sözcüğünü bir değere geçirebilirsiniz, klonlanmak istemezsiniz.

Bu değişkenleri depolama işaretçileri veya depolama referans türünün yerel değişkenleri olarak tanımlarız.

Bir işlevdeki herhangi bir depolama referanslı değişken, her zaman sözleşmenin depolamasında önceden tahsis edilmiş bir veri parçasına atıfta bulunur. Başka bir deyişle, bir depolama başvurusu her zaman bir durum değişkenine başvurur.

Diğer birçok yönetişim protokolü için temel olarak kullanılan çok popüler Akıllı Sözleşme Yönetişim protokolünün Solidity kodunu kullanalım: Compound.

Gerçek dünya örneği — Compound

Compound
Compound

GovernorAlpha akıllı sözleşme, yönetişim protokolünün oluşturulmasında etkili olmuştur. Bu sözleşme, yalnızca Compound için değil, aynı zamanda Uniswap veya Indexed Finance için de yönetişim alanında barebone olarak kullanılır.

GovernorAlpha‘nın temel işlevlerinden birine bakalım. propose(…) işlevi adından da anlaşılacağı gibi, yeni bir teklif oluşturmayı sağlar (örneğin: bir cToken‘in faiz oranını değiştirme). Aşağıya bakarsanız, daha önce anlattıklarımızdan iki örnek göreceksiniz:

compound protocol/contracts/Governance/GovernorAlpha.sol

153. satırında, proposalId yerel değişkenine, proposalCount durum değişkeninin değeri atanır. Bu yerel değişken temel tipte (uint) olduğundan, değer sözleşme deposundan (proposalCunt durum değişkeninden) yerel değişkene (yığın üzerinde) kopyalanır/klonlanır. Yerel değişkende yapılan herhangi bir değişiklik, sözleşme deposuna yayılmaz.

Compound‘da bu satır, yeni teklif kimliğini yerel olarak kaydetmek için kullanılır (proposalCount satırı 152 artırılarak oluşturulur). Bu da biraz gaz tasarrufu sağlar. 154 ve 157. satırlara bakın. proposalId yerine değişken proposalCount (gerçek durum değişkeni) olsaydı, bu, sözleşme deposunu iki kez okurdu.

Satır 154: Yeni proposalId kullanılarak bir newProposal oluşturulur. newProposal değişkeni bir struct (karmaşık tip) olduğundan, bu değişkeni daha sonra manipüle edip düzenleyeceğimiz zaman EVM’nin üzerinde çalışmasını istediğimiz veri konumunu belirtmeliyiz.

Bu örnek bir storage referansı kullanır.

  • Bu ne anlama geliyor? newProposal, sözleşme deposundaki bir yeri ifade eder.
  • Sözleşmeli depolamada hangi yeri ifade ediyor? Proposal eşlemesi içindeki bir Proposal‘ı ifade eder.
  • Hangi Proposal? Teklif, eşlemede proposalId tarafından belirtilir.

Bu storage anahtar sözcüğü ne anlama geliyor? newProposal değişkeninde yapılan her değişikliğin sözleşme deposuna veri yazılmasına neden olacağı anlamına gelir. 157. satırdan başlayarak, yeni Teklifin tüm detaylarının Propsal yapı üyeleri aracılığıyla birbiri ardına yazıldığını görebilirsiniz. Bu satırların her biri sözleşme deposuna yazar.

İşlev yürütüldüğünde, yeni Teklif, sözleşme deposuna kaydedilecek ve değişiklikler devam edecektir.

Depolama referansları ile aslında neler oluyor?

Aşağıdaki örneğe bir göz atın. Aynı yönetim konusuna dayanmaktadır. Bir depolama referansı kullanarak depolamadan kopyalama yaparken kullanılan EVM işlem kodlarını ayrıntılı olarak gösterir.

pragma solidity ^0.8.0;

contract Voting {

    uint256 votesCount;

    struct Vote {
        bool hasVoted;
        string vote;
    }

    mapping(address => Vote) votes;
    
    // ; opcodes
    // PUSH1 00 ; push number 0 on the stack (for slot 0)
    // SLOAD    ; load value at storage slot 0 (= `votesCount`)
    function getVotesCount() public view returns (uint256) {
        
        uint256 currentVotesCount = votesCount;
        return currentVotesCount;
    }

    // ; opcodes
    // PUSH1 00
    // PUSH1 01 ; push 1 on the stack (storage slot nb 1 for `votes`)
    // PUSH1 00
    // CALLER
    // PUSH20 ffffffffffffffffffffffffffffffffffffffff
    // AND
    // PUSH20 ffffffffffffffffffffffffffffffffffffffff
    // AND
    // DUP2
    // MSTORE
    // PUSH1 20
    // ADD
    // SWAP1
    // DUP2
    // MSTORE
    // PUSH1 20
    // ADD
    // PUSH1 00
    // SHA3
    // SWAP1
    // POP
    // PUSH1 01 ; push `true` for `hasVoted` onto the stack
    // DUP2
    // PUSH1 00
    // ADD
    // PUSH1 00
    // PUSH2 0100
    // EXP
    // DUP2
    // SLOAD  ; load the value located at the storage reference 
    // DUP2
    // PUSH1 ff
    // MUL
    // NOT
    // AND
    // SWAP1
    // DUP4
    // ISZERO
    // ISZERO
    // MUL
    // OR
    // SWAP1
    // SSTORE ; update the storage by marking it `hasVoted`
    function hasVoted() public {
        Vote storage callerVoteDetails = votes[msg.sender];
        callerVoteDetails.hasVoted = true;
    }

}

İlk işlev getVotesCount(), değeri yığından kopyalar ve ardından onu döndürür. Değerin depodan yığına SLOAD ile yüklendiğini görebiliriz. currentVotesCount değişkeninde yapılan herhangi bir değişiklik, depolamaya geri yayılmayacaktır.

Aksine ikinci örnek, bir storage referansı içerir. Vote yapısında hasVoted üyesine yeni bir değer atadığımız anda, depolama güncellenir ve SSTORE işlem kodunu görebiliriz.

Bu örnek, bir storage referans değişkenine yeni değer atamanın sözleşme depolamasını güncellediğini gösterir. EVM, bunu bir SSTORE talimatı gerçekleştirmek için bir “tetikleyici” olarak anlar.

Aksine, önceki örnekte gösterildiği gibi, bir depolama değişkeninden değer atanan elementer değişkenler referans oluşturmaz, sadece değeri depolamadan yığına kopyalar. EVM bunu basitçe bir SLOAD talimatı yapmak için bir tetikleyici olarak anlar.

Assembly ve Yul’den depolamaya erişim

Bir depolama yuvası ve depolama ofseti belirterek satır içi derlemede sözleşme depolamasını okuyabilir ve yazabilirsiniz.

Depolamadaki bazı değişkenlerin tek bir tam depolama yuvasını işgal etmediğini, bazen birlikte paketlendiklerini daha önce görmüştük.

Ayrıca bir işlem kodu olarak SLOAD‘ın parametre olarak yalnızca depolama yuvası numarasını kabul ettiğini ve bu yuvanın altında saklanan tam bytes32 değerini döndürdüğünü gördük.

Peki, aynı depolama yuvasındaki diğer birçok durum değişkeni arasında paketlenmiş bir durum değişkeni nasıl okunur?

Örnek olarak aşağıdaki sözleşmeyi alın:

contract Storage {
    uint64 a;
    uint64 b;
    uint128 c;
}

Solidity belgeleri aşağıdakileri açıklar:

Yerel depolama değişkenleri veya durum değişkenleri için, tek bir tam depolama yuvasını işgal etmedikleri için tek bir Yul tanımlayıcısı yeterli değildir.

Bu nedenle, “address” bir yuva ve o yuvanın içindeki bir bayt ofsetinden oluşur.

Bu nedenle, bir değişkenin “adresi” iki bileşenden oluşur:

  • slot numarası: değişkenin bulunduğu yer.
  • değişkenin başladığı bayt ofseti (o yuvanın içinde).

Daha iyi anlamak için bazı temel derleme kodlarıyla devam edelim. Aşağıdaki sözleşmeye ve işlevlerine bir göz atın:

contract Storage {
    uint64 a = 1;
    uint64 b = 2;
    uint128 c = 3;
    function getSlotNumbers() public view returns(uint256 slotA, uint256 slotB, uint256 slotC) {
        assembly {
            slotA := a.slot
            slotB := b.slot
            slotC := c.slot
    
        }
    }
    function getVariableOffsets() public view returns(uint256 offsetA, uint256 offsetB, uint256 offsetC) {
        assembly {
            offsetA := a.offset
            offsetB := b.offset
            offsetC := c.offset
    
        }
    }
}

Bu iki işlevi Remix aracılığıyla çalıştırmak aşağıdaki çıktıları verir:

getVariableOff…
0 : uint256: offsetA 0
1 : uint256: offsetB 8
2 : uint256: offsetC 16

getVariableSlo…
0 : uint256: offsetA 0
1 : uint256: offsetB 0
2 : uint256: offsetC 0

Satır içi montaj ve Yul ile,

c değişkeninin işaret ettiği yuvayı almak için c.slot‘u ve bayt ofseti almak için c.offset‘i kullanırsınız. c‘nin kendisini kullanmak bir hataya neden olur.

function ReadVariableC() public view returns (uint64 value) {
    assembly {
        value := sload(c)
    }
}

Söylenmesi gereken bir şey de, satır içi derlemede, bir depolama değişkeninin .slot veya .offset kısmına atayamayacağınızdır.

function doesNotCompile() public {
    assembly {
        a.slot := 8
        a.offset := 9
    }
}

Yul’daki depolama işaretçilerinin ofseti nedir?

İşlev gövdelerinde, bazı değişkenler depolama işaretçileri/depolama referansları olabilir. Örneğin, buna sturct, array ve mapping dahildir. Bu tür değişkenler için, Yul’de .offset her zaman sıfır olacaktır çünkü bu tür değişkenler her zaman tam bir depolama yuvasını işgal eder ve diğer değişkenlerle birlikte depoda sıkıca paketlenemez.

Sonuç Bağlamı : Solidity Eğitimi Depolama

Akıllı bir sözleşmenin depolanması, içindeki verileri başlatmak veya değiştirmek için yazılması maliyetlidir. Sözleşme deposundan veri okumak ücretsiz olsa da, bu okuma işlemleri durum değiştiren bir işlemin parçasıysa, akıllı sözleşmenin depolanmasına okumayla ilişkili gaz maliyetini yine de göz önünde bulundurmalısınız.

Depolamada çalıştırmanın yüksek gaz maliyeti nedeniyle, Solidity belgeleri önemli bir hususu belirtir:

kalıcı depolamada sakladığınız şeyi sözleşmenin çalışması için gerekene en aza indirmelisiniz.

İlgili gaz maliyetini en aza indirmek için mümkün olduğunda belirli verilerin sözleşmeli depolama dışında saklanması önerilir.

Akıllı sözleşme geliştirme yolculuğunuz hakkında daha iyi rehberlik almak için Solidity nedir? Ethereum Akıllı Sözleşmelerinin Dili Rehberi içeirğimize göz atın. Dilerseniz Yeni Başlayanlar için Solidity – Akıllı Sözleşme Geliştirme Hızlandırılmış Kursuna katılın.

Çalışmaya nereden başlayacğaım diyenler için Blockchain ​​Developer Olmak İçin Yol Haritası içeriğine de muhakkak bakın.

Bu makaleyi okuduğunuz için teşekkürler! Bana destek olmak isterseniz;

Beni TwitterLinkedin ve YouTube‘da takip edin.

Kısa bir yorum bırakmayı UNUTMAYIN!

Hasan YILDIZ, Girişimci. Doktora Öğrencisi. Yazmayan YAZILIMCI. Veri Şeysi. Eğitmen...

Yazarın Profili
İlginizi Çekebilir

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir