本篇預計閱讀時間 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);
}
}