2022-05-16 10:49 更新

庫類似于合約,但它們的目的是它們只在特定地址部署一次,并且使用 EVM 的DELEGATECALL (CALLCODE直到 Homestead)功能重用它們的代碼。這意味著如果調(diào)用庫函數(shù),它們的代碼將在調(diào)用合約的上下文中執(zhí)行,即this指向調(diào)用合約,尤其是可以訪問調(diào)用合約的存儲(chǔ)。由于庫是一段孤立的源代碼,它只能訪問調(diào)用合約的狀態(tài)變量,如果它們被顯式提供(否則它無法命名它們)。如果庫函數(shù)DELEGATECALL不修改狀態(tài)(即如果它們是view或pure函數(shù)),因?yàn)閹毂患俣闊o狀態(tài)的。特別是,不可能銷毀庫。

筆記

在 0.4.20 版本之前,可以通過繞過 Solidity 的類型系統(tǒng)來銷毀庫。從那個(gè)版本開始,庫包含一種不允許直接調(diào)用狀態(tài)修改函數(shù)的機(jī)制DELEGATECALL(即沒有)。

庫可以被視為使用它們的合約的隱含基礎(chǔ)合約。它們?cè)诶^承層次結(jié)構(gòu)中不會(huì)顯式可見,但對(duì)庫函數(shù)的調(diào)用看起來就像對(duì)顯式基礎(chǔ)合約的函數(shù)的調(diào)用(使用限定訪問,如L.f())。當(dāng)然,對(duì)內(nèi)部函數(shù)的調(diào)用使用內(nèi)部調(diào)用約定,這意味著所有內(nèi)部類型都可以傳遞,并且存儲(chǔ)在內(nèi)存中的類型將通過引用傳遞而不是復(fù)制。為了在 EVM 中實(shí)現(xiàn)這一點(diǎn),從合約調(diào)用的內(nèi)部庫函數(shù)的代碼以及從其中調(diào)用的所有函數(shù)將在編譯時(shí)包含在調(diào)用合約中,并且JUMP將使用常規(guī)調(diào)用而不是DELEGATECALL.

筆記

當(dāng)涉及到公共函數(shù)時(shí),繼承類比就失效了。調(diào)用公共庫函數(shù)L.f()會(huì)導(dǎo)致外部調(diào)用(DELEGATECALL 準(zhǔn)確地說)。相反,當(dāng)是當(dāng)前合約的基礎(chǔ)合約時(shí),A.f()是內(nèi)部調(diào)用。A

以下示例說明了如何使用庫(但使用手動(dòng)方法,請(qǐng)務(wù)必查看using for以獲得更高級(jí)的示例來實(shí)現(xiàn)集合)。

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


// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data {
    mapping(uint => bool) flags;
}

library Set {
    // Note that the first parameter is of type "storage
    // reference" and thus only its storage address and not
    // its contents is passed as part of the call.  This is a
    // special feature of library functions.  It is idiomatic
    // to call the first parameter `self`, if the function can
    // be seen as a method of that object.
    function insert(Data storage self, uint value)
        public
        returns (bool)
    {
        if (self.flags[value])
            return false; // already there
        self.flags[value] = true;
        return true;
    }

    function remove(Data storage self, uint value)
        public
        returns (bool)
    {
        if (!self.flags[value])
            return false; // not there
        self.flags[value] = false;
        return true;
    }

    function contains(Data storage self, uint value)
        public
        view
        returns (bool)
    {
        return self.flags[value];
    }
}


contract C {
    Data knownValues;

    function register(uint value) public {
        // The library functions can be called without a
        // specific instance of the library, since the
        // "instance" will be the current contract.
        require(Set.insert(knownValues, value));
    }
    // In this contract, we can also directly access knownValues.flags, if we want.
}

當(dāng)然,您不必按照這種方式使用庫:也可以在不定義結(jié)構(gòu)數(shù)據(jù)類型的情況下使用它們。函數(shù)也可以在沒有任何存儲(chǔ)引用參數(shù)的情況下工作,并且它們可以有多個(gè)存儲(chǔ)引用參數(shù)并且可以在任何位置。

對(duì)和的調(diào)用Set.contains都 編譯為對(duì)外部合約/庫的調(diào)用 ( )。如果您使用庫,請(qǐng)注意執(zhí)行了實(shí)際的外部函數(shù)調(diào)用。 ,并且將在此調(diào)用中保留它們的值(在 Homestead 之前,因?yàn)槭褂?并且 更改了)。Set.insertSet.removeDELEGATECALLmsg.sendermsg.valuethisCALLCODEmsg.sendermsg.value

以下示例顯示了如何使用存儲(chǔ)在內(nèi)存中的類型和庫中的內(nèi)部函數(shù)來實(shí)現(xiàn)自定義類型,而無需外部函數(shù)調(diào)用的開銷:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

struct bigint {
    uint[] limbs;
}

library BigInt {
    function fromUint(uint x) internal pure returns (bigint memory r) {
        r.limbs = new uint[](1);
        r.limbs[0] = x;
    }

    function add(bigint memory a, bigint memory b) internal pure returns (bigint memory r) {
        r.limbs = new uint[](max(a.limbs.length, b.limbs.length));
        uint carry = 0;
        for (uint i = 0; i < r.limbs.length; ++i) {
            uint limbA = limb(a, i);
            uint limbB = limb(b, i);
            unchecked {
                r.limbs[i] = limbA + limbB + carry;

                if (limbA + limbB < limbA || (limbA + limbB == type(uint).max && carry > 0))
                    carry = 1;
                else
                    carry = 0;
            }
        }
        if (carry > 0) {
            // too bad, we have to add a limb
            uint[] memory newLimbs = new uint[](r.limbs.length + 1);
            uint i;
            for (i = 0; i < r.limbs.length; ++i)
                newLimbs[i] = r.limbs[i];
            newLimbs[i] = carry;
            r.limbs = newLimbs;
        }
    }

    function limb(bigint memory a, uint index) internal pure returns (uint) {
        return index < a.limbs.length ? a.limbs[index] : 0;
    }

    function max(uint a, uint b) private pure returns (uint) {
        return a > b ? a : b;
    }
}

contract C {
    using BigInt for bigint;

    function f() public pure {
        bigint memory x = BigInt.fromUint(7);
        bigint memory y = BigInt.fromUint(type(uint).max);
        bigint memory z = x.add(y);
        assert(z.limb(1) > 0);
    }
}

可以通過將庫類型轉(zhuǎn)換為類型來獲取庫的地址address,即使用address(LibraryName).

由于編譯器不知道庫將部署到的地址,因此編譯后的十六進(jìn)制代碼將包含表單的占位符__$30bbc0abd4d6364515865950d3e0d10953$__。占位符是完全限定庫名稱的 keccak256 哈希的十六進(jìn)制編碼的 34 個(gè)字符前綴,例如libraries/bigint.sol:BigInt,如果庫存儲(chǔ)在目錄中調(diào)用的文件bigint.sol中l(wèi)ibraries/。此類字節(jié)碼不完整,不應(yīng)部署。占位符需要替換為實(shí)際地址。您可以通過在編譯庫時(shí)將它們傳遞給編譯器或使用鏈接器更新已編譯的二進(jìn)制文件來做到這一點(diǎn)。有關(guān)如何使用命令行編譯器進(jìn)行鏈接的信息,請(qǐng)參閱 庫鏈接。

與合約相比,庫在以下方面受到限制:

  • 他們不能有狀態(tài)變量
  • 他們不能繼承也不能被繼承
  • 他們無法接收以太幣
  • 他們不能被摧毀

(這些可能會(huì)在稍后解除。)

庫中的函數(shù)簽名和選擇器?

雖然可以對(duì)公共或外部庫函數(shù)進(jìn)行外部調(diào)用,但此類調(diào)用的調(diào)用約定被認(rèn)為是 Solidity 內(nèi)部的,與為常規(guī)合約 ABI指定的不同。外部庫函數(shù)支持比外部合約函數(shù)更多的參數(shù)類型,例如遞歸結(jié)構(gòu)和存儲(chǔ)指針。出于這個(gè)原因,用于計(jì)算 4 字節(jié)選擇器的函數(shù)簽名是按照內(nèi)部命名模式計(jì)算的,并且合約 ABI 中不支持的類型的參數(shù)使用內(nèi)部編碼。

以下標(biāo)識(shí)符用于簽名中的類型:

  • 值類型、非存儲(chǔ)string和非存儲(chǔ)bytes使用與合約 ABI 中相同的標(biāo)識(shí)符。

  • 非存儲(chǔ)數(shù)組類型遵循與合同 ABI 中相同的約定,即<type>[]用于動(dòng)態(tài)數(shù)組和 <type>[M]固定大小的M元素?cái)?shù)組。

  • 非存儲(chǔ)結(jié)構(gòu)由它們的完全限定名稱引用,即C.Sfor 。contract C { struct S { ... } }

  • 存儲(chǔ)指針映射分別使用where和是映射的鍵和值類型的標(biāo)識(shí)符。mapping(<keyType> => <valueType>) storage<keyType><valueType>

  • 其他存儲(chǔ)指針類型使用其對(duì)應(yīng)的非存儲(chǔ)類型的類型標(biāo)識(shí)符,但在其后附加一個(gè)空格storage。

參數(shù)編碼與常規(guī)合約 ABI 相同,除了存儲(chǔ)指針,它們被編碼為 uint256引用它們指向的存儲(chǔ)槽的值。

與合約 ABI 類似,選擇器由簽名的 Keccak256-hash 的前四個(gè)字節(jié)組成。它的值可以使用.selector如下成員從 Solidity 獲得:

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

library L {
    function f(uint256) external {}
}

contract C {
    function g() public pure returns (bytes4) {
        return L.f.selector;
    }
}

庫的呼叫保護(hù)?

正如介紹中提到的,如果使用 aCALL而不是DELEGATECALLor來執(zhí)行庫的代碼,除非調(diào)用or函數(shù)CALLCODE,否則它將恢復(fù)原狀。viewpure

EVM 沒有為合約提供直接的方法來檢測(cè)它是否被調(diào)用CALL,但是合約可以使用ADDRESS操作碼來找出它當(dāng)前運(yùn)行的“位置”。生成的代碼將此地址與構(gòu)建時(shí)使用的地址進(jìn)行比較,以確定調(diào)用方式。

更具體地說,庫的運(yùn)行時(shí)代碼總是以 push 指令開始,它在編譯時(shí)是 20 字節(jié)的零。當(dāng)部署代碼運(yùn)行時(shí),這個(gè)常量在內(nèi)存中被當(dāng)前地址替換,修改后的代碼存儲(chǔ)在合約中。在運(yùn)行時(shí),這會(huì)導(dǎo)致部署時(shí)間地址成為第一個(gè)被壓入堆棧的常量,并且調(diào)度程序代碼將當(dāng)前地址與任何非視圖和非純函數(shù)的該常量進(jìn)行比較。

這意味著存儲(chǔ)在鏈上的庫的實(shí)際代碼與編譯器報(bào)告的代碼不同 deployedBytecode。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)