深入淺出以太坊智能合約

這篇文會從智能合約(Smart Contract)的介紹到實作一份簡單的智能合約(Remix 提供的Example),但此文與其他智能合約教學不同,最重要的目標不在於教你Coding,而是讓剛入門的人先了解撰寫智能合約有哪些眉角,這是因為我認為Coding誰都會(宣告參數、實作方法…),但如何在區塊鏈的宗旨下寫好一份智能合約不只這些。讓我們開始吧!

智能合約(Smart Contract)

智能合約是一種以契約為形式的數字交易協定,最初概念來自於 Nick Szabo 的 Smart Contracts: Building Blocks for Digital Markets,他認為自動販賣機就是現實中智能合約的典型例子,我給你15 NTD,你給我麥香,我給你25 NTD,你給我黑松沙士,這是透過電腦履行承諾的一種實現,無需第三方的介入,這是在二十世紀末提出來的想法 (1994–1996)。

有了智能合約的想法,或許我們就會產生疑問,那我要怎麼相信電腦?如果自動販賣機被駭,我付了 25 NTD 卻給我麥香,或我的錢直接被吃掉,安餒剛丟?然而,在2008年,中本聰 (Satoshi Nakamoto) 提出的 Bitcoin: A peer-to-peer electronic cash system 解決了這樣的疑慮。Bitcoin 透過 Peer-to-Peer Network 裡節點 (nodes, miners) 間的 PoW (Proof-of-Work) 共識機制 (Consensus Algorithm) 達成一個不信任網路裡的信賴機制。至於細節,之後再寫一篇好好講解 Bitcoin: A peer-to-peer electronic cash system 這篇paper。

有鑒於比特幣的成功,2013年, 具備圖靈完整 (Turing Completeness) 的以太坊 (Ethereum) 問世,由V神 Vitalik Buterin所提出,並在2014年發表Ethereum的白皮書 (White paper) : A NEXT GENERATION SMART CONTRACT & DECENTRALIZED APPLICATION PLATFORM,與比特幣不同的地方是,Ethereum是運行智能合約的區塊鏈平台,這大大提升智能合約的應用潛力。

cover.jpg

以太坊智能合約特性

A. 合約與執行結果都會儲存在區塊鏈上,因此每筆交易 (Transaction) 都保有不可篡改性 (Tamper-proof)、永久性 (Permanency) 以及公開透明性 (Transparency)。

B. Smart Contract 一但部署 (Deployment) 後便不可更改,這也就代表合約保有不可逆特性,因此以太坊提供許多測試鏈 (Ropsten, Kovan, Rinkeby, Goerli) 給智能合約撰寫者測試合約,讓在以太坊的用戶可以幫忙Debug,那當然你也可以架在本地 (Ganache) 自行測試。

ganache.webp

C. 以太坊的 Smart Contract 運行於 EVM(Ethereum Virtual Machine) 上,合約的撰寫語言為 Solidity (靜態型語言),撰寫完畢的智能合約會被轉譯成EVM bytecode提供每個節點透過EVM去執行合約。

evm-bytecode.webp

D. 為了提供節點運算的獎勵,合約部署或執行合約時需要耗費 gas,而gasgas Price就是節點獲得的獎勵 (單位: ether),gas是由code的效率去決定,若這動作會吃很多運算資源,gas越高,gas Price則是影響執行交易的速度,gas Price 訂得越高,節點越想幫你執行合約,因此gasgas Price可以想像成是服務費。若沒有足夠數量的以太幣則無法執行合約。

Remix

Web IDE for Solidity

Remix 是一種網頁式IDE(Intergrated Development Environment),用於開發 Solidity 所撰寫的智能合約。其內建功能包括:

  • 編譯
  • 除錯
  • 合約模擬 (本身提供15個虛擬帳號,各帳號含100個ether)
  • 合約部署

remix.webp

在上圖可以看到我特別匡選了Compiler Version,由於Solidity 語言仍處於持續開發階段,變動非常的頻繁,因此需要注意各個版本不同的語法差異。

MetaMask

MetaMask是一個以Google Chrome為Extension的以太坊錢包,在這個錢包你可以建立、匯入帳戶,進行交易或合約的執行,以及連接至不同的以太坊區塊鏈。

metamask.png

MetaMask is an extension for accessing Ethereum enabled distributed applications, or "Dapps" in your normal Chrome browser! The extension injects the Ethereum web3 API into every website's javascript context, so that dapps can read from the blockchain. MetaMask also lets the user create and manage their own identities, so when a Dapp wants to perform a transaction and write to the blockchain, the user gets a secure interface to review the transaction, before approving or rejecting it. Because it adds functionality to the normal browser context, MetaMask requires the permission to read and write to any webpage. You can always "view the source" of MetaMask the way you do any Chrome extension, or view the source code on Github: https://github.com/MetaMask/metamask-plugin

Solidity

Solidity是一種合約導向式語言,最早是由Gavin Wood在2014年提出,後期則以Christian Reitwiessner所領導的以太坊團隊Solidity接手開發。Solidity參照了ECMAScript的語法概念,因此有寫過Javascript的人會較好上手。在開始Coding前,先針對以太坊裡Solidity合約的一些專有名詞做介紹:

  • 貨幣單位: ether
  • address 分為兩種
    • 使用者地址 (EOA, externally owned account)
    • 合約地址 (contract address): 會有一份code和contract address綁在一起。同一份code在不同contract address上就是不同合約。
  • transaction
    • 可為單純的轉帳交易
    • 用來執行合約(同時也可附錢)
  • 合約內狀態(state)和動作(function)
    • state:可永久保留,使用者用來記錄合約的相關資訊。因為每個節點都要儲存一份一樣的資料,因此用到state耗費的gas特別高。
    • function:對這合約狀態產生影響的動作。

接下來再針對Solidity的語法做詳細的介紹:

Variables
  1. 宣告變數的方式: 型態 能見度 變數名稱 ( type visibility variable_name) e.g. address public contractOwner 型態則有 bool, uint, address, mapping, bytes, string, struct…
  2. 能見度 visibility : public,外界可以直接讀取其值 private(default),外界不可讀取其值
  3. mapping: 類似Dictionary。 e.g. mapping(address => uint) deposits; deposits[0x123456789abcdef] = 10; 較推薦用這種方式儲存群體資訊,用array吃的gas會很多。因為Solidity儲存Array的方式為:
struct Array{ mapping(uint => someType) items; uint length; }
  1. msg: 這個transaction相關的資訊。 msg.sender: 該transaction的發起人。 msg.value: 該transaction附帶的錢。
  2. address address.balance: 得知該address的餘額。 address.send(amount): 送錢到該address。
  3. memory: 暫存用的參數型態。
  4. storage: 指到state的參數,直接改到該參數。
Functions
  1. 函式的能見度 (visibility): public(default) private: 只有這個contract。 internal: 這個contract和繼承這個contract的contract。 external: 除了這個contract和繼承這個contract的contract之外,即internal的相反。
  2. payable: 函式是否可以收錢。
  3. 條件檢查 (Error Handling) e.g. require(bool condition, string memory message) 在執行function第一步可以先做條件檢查,若沒通過條件則不執行此function。因智能合約大多會處理交易,因此會在許多地方做條件檢查,怕一步錯步步錯,例如有些function只能contract owner才有權限: require(msg.sender == contractOwner, “Sender not authorized.”);
  4. modifier: 在執行某個function前需要做檢查或者預先處理時可以使用modifier,並且可以達到reuse。以下面為例,先做 onlyBy 的確認,通過了,才做changeOwner裡的動作。
  5. view : 這個形態的function只能call參數,不能改動參數值,因此不會cost gas。
modifier onlyBy(address _account){ require(msg.sender == _account, "Sender not authorized."); _; } function changeOwner(address _newOwner) public onlyBy(contractwner){ contractOwner = _newOwner; }

實作智能合約

Coding

做完一堆前情提要後,終於可以來實作智能合約了,這邊就以Remix提供的Bollot.sol 做示範,這個合約是實作一個投票機制,在合約部署時會提供宣告許多Proposals讓用戶去投票。許多智能合約教學都用投票來做示範,因為除了防止金錢交易的弊病外,許多人也認為智能合約可以解決黑箱投票的詬病。事不宜遲,來一行一行講解吧!

solidity-version.png

首先要先定義此合約是用哪個版本的Solidity所撰寫。

state-variables.webp

再來宣告此合約的state參數:

Voter: 每個Voter都有自己的權重,也會去紀錄此Voter有無投過票,再來是delegate,每個Voter可以把自己的投票權轉移到一個代表 (EOA address),那這個delegate的概念很重要,ERC20也會實作這樣的概念,之後再好好講ERC20,最後是這位Voter決定投哪個Proposal。

Proposal: 記錄此提案的名字,長度最多為32 bytes,還有獲得幾張票。

chairperson: 主導提案的主席。

voters: 因為address (EOA)是unique,所以用address (EOA) 當key去指到特定的Voter。

proposals: 有哪些提案。 constructor.png

在合約部署時會在建構子去帶入有哪些Proposals,是個bytes32的矩陣。然後將部署合約的人,也就是Contract Owner,指定為chairperson,當然,主席也可以投票,且每個人票票平等,weight是1。

給予投票權.png

這裡做了三次條件檢查,第一個是只有主席可以給予投票權給特定的EOA,再來是該Voter不能有投票過,這裡注意一點,第一次在voters這個mapping裡初始這個EOA的Voter時,uint預設會是0,因此會在下面將weight調成1,代表voters已有此EOA。

delegate.png

接下來是實作將投票權轉移,首先會有兩個條件檢查,第一個是若該Voter以投票,那代表他投完了,就沒有投票權可以做轉移,第二個是將投票權轉移到自己身上不合邏輯,所以也會擋掉。那接下來是while裡的address(0) 這代表address的初始值,所以代表被指定的人若也有另指定的人,那就代表voters[to].delegate 這個address身負重任囉!他要一次扛兩個以上的投票權。那接下來就是針對這個被指定的人的投票權中做處理。

vote.png

再來是投票的動作,若msg.sender 沒投票權,那就掰掰~如果他也投過了,那也掰掰~若條件檢查都是false,就改成已投票並將proposal的voteCount做++。

看誰贏.png

最後是宣布勝利者,那就是針對誰的voteCount最多去決定。

Compile

compile.png

在這個頁面要記得確認Compiler的版本是否符合你撰寫智能合約時宣告的版本。

deploy.png

接下來就可以部署了,首先看到Environment,有三個選項可以選,一是JavaScript VM,指將智能合約部署到Remix提供的VM上去運行,二是Injected Web3,指將智能合約部署到正在運行的鏈上,這邊會去抓MetaMask現在的鏈,三是Web3 Provider,指幫你部署到你local端的鏈上,可以是Ganache。然後選擇用哪個account去部署合約,以及要用多少的gas value去進行部署的動作,越多部署地越快,最後是在Deploy那格輸入Proposals array,因為他是bytes32[],所以你可以用下面這個function把指定的string轉成bytes32。

string to bytes32.png

deployed.png

看到這裡就代表你已經成功部署此合約,接下來就可以盡情地去執行Transactions。

Gas

前面有提到gas,基本上就是你在以太坊裡執行任何動作都會有運算資源的消耗,這些運算資源來自那些節點,你就得給予這些節點服務費去幫你執行合約。因此這邊會稍微介紹怎麼計算自己的Smart Contract會花多少gas。

  1. 部署合約以及Constructor: 536467 gas
  2. 每個Transaction的低消: 21000 gas
  3. Data: Gtxdatazero : 4 gas/byte Gtxdatanonzero : 68 gas/byte

不一樣的動作會花費不一樣的gas,詳細可以瀏覽Ethereum的Yellow Paper或是這個Sheet

Ethereum yellow paper.png

如果是用Remix去撰寫智能合約的話,在Compile Page可以去看gas estimates,這裡會一一列出來。

Gas estimates.png

總結

講到這邊,應該看得出來寫一份智能合約並不是打打Code,Compile過了就好的一件事,必須先了解為何需要區塊鏈的節點去執行智能合約,

因為智能合約是不可逆的契約,所以需要有條件檢查以防萬一, 為何Solidity分那麼多Variable types,因為每個動作都會吃計算資源,所以gas極為重要,也需要知道gas怎麼計算。 一份好的智能合約就是要夠安全、夠保險、花夠少gas,這樣才會有夠多人用你的智能合約,符合區塊鏈的宗旨!