Solidity 教學:引用 Imports and 函式庫 Libraries

本篇預計閱讀時間 15 分鐘, 實作時間 50 分鐘。

在開始之前先來思考底下的練習題,閱讀後開始練習。

練習題

  • 練習題 1

    • 如何設計一個多人管理的智能合約保管箱來用於公司帳戶、家庭零用錢錢包等。

在 Solidity 中,一個 .sol 檔中可以有多個 contract 物件,故可以藉由引用的方式將其它檔案內的contract 引入至當前檔案中。

 

引入的方法如下:

  • 若需要從 Global-Level 引入整個檔案:

    • import "filename"

  • 若需引用整個檔案的成員:

    • import * as a symbolName from "filename"

  • 只想引用檔案中的特定成員:

    • import {symbol1 as alias, symbol2} from "filename"

 

Library

Library 非常類似於 contracts,但我們不能在裡面宣告任何的 state variables,也不可以傳送任何 ether,只會被佈署一次。在引入 Library 的時候可以將其視為繼承來的父合約,這樣去理解其中的函式可視性與 this 的使用會比較好懂。

使用範例:

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

library Lib {
    function add(uint a, uint b) pure internal returns(uint) {return a + b;
    }
}
contract A {
    using Lib for uint;

function add(uint a, uint b) pure external returns(uint) {
    return a.add(b);
    }
}

在 Solidity 裡面有兩種 Libraries 類型:

  • deployed:有自己的合約地址,可以被智能合約在執行時調用

  • embedded:當所有的 library 函數皆為 internal;沒有自己的合約地址,會變成我們合約的一部分程式碼。

//SPDX-License-Identifier: MIT
// Embedded (function is internal)
library Lib_Embedded {
    function add(uint a, uint b) pure internal returns(uint) {
        return a + b;
    }
}
//Deployed (function is public)
library Lib_Deployed {
    function add(uint a, uint b) pure public returns(uint) {
        return a + b;
    }
}

Libraries & Using ... for

  • Libraries 使用上有點類似 Contracts

  • 程式碼運用了 DELEGATECALL 的特性可以被重複使用

  • Libraries 運作上類似直接從「呼叫引入處」貼上來源程式碼至合約內

  • Libraries 不具有自我摧毀的功能,因此從定義上不能被摧毀

  • 相關限制(在未來版本有可能會改變)

    • 不具有狀態變數(State variables)

    • 不可進行繼承和被繼承

    • 不可接收 Ether

Import

如果我們要從 local 的方式 import 檔案,可以假設 Folder Structure 如下:

├── Import.sol
└── Foo.sol

創建一個名為 Foo.sol 的檔案:

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

struct Point {
    uint x;
    uint y;
}

error Unauthorized(address caller);

function add(uint x, uint y) pure returns (uint) {
    return x + y;
}
contract Foo {
    string public name = "Foo";
}

接下來創建一個名為 Import.sol 的檔案,並且在裡面 import Foo.sol

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

// import Foo.sol from current directory
import "./Foo.sol";

// import {symbol1 as alias, symbol2} from "filename";
import {Unauthorized, add as func, Point} from "./Foo.sol";

contract Import {
    // Initialize Foo.sol
    Foo public foo = new Foo();

    // Test Foo.sol by getting it's name.
    function getFooName() public view returns (string memory) {return foo.name();
    }
}

當然也可以繼承其他地方來的合約:

import "./someothercontract.sol";

contract newContract is SomeOtherContract {

}

如果要從 External 的地方像是 GitHub import 可以藉由複製 url 方式 import:

//https://github.com/owner/repo/blob/branch/path/to/Contract.sol
import "https://github.com/owner/repo/blob/branch/path/to/Contract.sol";

/* Example import ECDSA.sol from openzeppelin-contract repo, release-v3.3 branch// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.3/contracts/cryptography/ECDSA.sol
*/
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.3/contracts/cryptography/ECDSA.sol";

OpenZeppelin

OpenZeppelin 是一個智能合約庫,其中包含了經過社區審查的 ERC 代幣標準、安全協議以及其他輔助工具,使用 OpenZeppelin 可以極大的提高 Solidity 合約的開發效率並保證合約的安全性。最常用的合約庫如下:

  • Ownable:

    • OpenZeppelin 的 Ownable 合約提供的 onlyOwner 模式是用來保護特定合約方法的訪問權限的基礎但非常有效的模式。

  • SafeMath:

    • 這個合約會進行所有必要的檢查,避免你的程式碼因算術運算而出現漏洞,或者因為溢位等因素而出現預期外的計算結果。

  • SafeCast:

    • 當我們在做型別轉換的時候有可能會出現精度遺漏或者溢位等問題,SafeCast 可以幫我們完成這些轉換而無需擔心溢出問題。

  • ERC20/ERC721/ERC1155 等代幣模式

使用 OpenZeppelin 需要特別注意 GitHub 上的分支,尤其是版本不同在引入到智能合約時會導致編譯錯誤或各種問題。舊版的 OpenZeppelin 使用上也需要格外小心,因為更新版本有可能是為了修補過往的漏洞或改善品質。

練習題解答

  • 練習題解答 1

    • 如何設計一個多人管理的智能合約保管箱來用於公司帳戶、家庭零用錢錢包等。

 

 

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

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ master/contracts/access/Ownable.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ master/contracts/utils/math/SafeMath.sol";

contract Allowance is Ownable {

    event AllowanceChanged(address indexed _forWho, address indexed _ byWhom, uint _oldAmount, uint _newAmount);

    mapping(address => uint) public allowance;

    function isOwner() internal view returns(bool) { 
        return owner() == msg.sender;
    }

    function setAllowance(address _who, uint _amount) public onlyOwner {

        emit AllowanceChanged(_who, msg.sender, allowance[_who], _ amount);

        allowance[_who] = _amount; 
}

    modifier ownerOrAllowed(uint _amount) {
        require(isOwner() || allowance[msg.sender] >= _amount, "You are not allowed!");
         _;
    }

    function reduceAllowance(address _who, uint _amount) internal ownerOrAllowed(_amount) {

        emit AllowanceChanged(_who, msg.sender, allowance[_who], allowance[_who] - _amount);

        allowance[_who] -= _amount; 
    }
}

contract SharedWallet is Allowance {

    event MoneySent(address indexed _beneficiary, uint _amount);     event MoneyReceived(address indexed _from, uint _amount);

    function withdrawMoney(address payable _to, uint _amount) public ownerOrAllowed(_amount) {

        require(_amount <= address(this).balance, "Contract doesn't own enough money");

        if(!isOwner()) { reduceAllowance(msg.sender, _amount);
}
        emit MoneySent(_to, _amount); _to.transfer(_amount);
}

    function renounceOwnership() public override onlyOwner { revert("can't renounceOwnership here");
}

    receive() external payable {
        emit MoneyReceived(msg.sender, msg.value);
    } 
}