智慧型合約結構
頁面最後更新時間: 2026年2月23日
智慧型合約是在以太坊地址運作的程式。 由可以在接收交易後執行的資料與函數組成。 此為智慧型合約組成的概覽。
先決條件
請務必先閱讀關於 智能合約 的內容。 此文件假設你已熟悉 JavaScript 或 Python 等程式語言。
資料
任何合約資料都必須指派到一個位置:storage 或 memory。 修改智慧型合約的存儲很麻煩,所以必須謹慎思考要將資料儲存至何處。
Storage
永久資料也稱為存儲,並由狀態變數表示。 這些值會永久儲存於區塊鏈上。 你需要聲明一個類型,以便於合約在編譯時可以追蹤在區塊鏈上需要多少存儲空間。
1// Solidity 範例2contract SimpleStorage {3 uint storedData; // 狀態變數4 // ...5}1# Vyper 範例2storedData: int128如果已編寫過物件導向程式語言,應該會熟悉大多數類型。 但是,如果你是剛接觸以太坊開發的新手,應該會對 address 感到陌生。
address 類型可以儲存一個以太坊地址,相當於 20 個位元組或 160 位元。 它會以十六進制的形式傳回,前綴是 0x。
其他類型包含:
- 布林值
- 整數
- 定點數
- 固定規模的位元組陣列
- 動態大小的位元組陣列
- 有理數與整數常值
- 字串常值
- 十六進位常值
- 列舉
如需更多說明,請參閱文件:
記憶體
僅在合約函數的執行生命週期儲存的值稱為記憶體變數。 由於這些變數不是永久儲存在區塊鏈上,所以使用成本要低得多。
在 Solidity 文件opens in a new tab中,深入了解 EVM 如何儲存資料 (儲存體、記憶體與堆疊)。
環境變數
除了在自已的合約上定義的變數外,還有一些特殊的全域變數。 它們主要用於提供有關區塊鏈或目前交易的資訊。
範例:
| 屬性 | 狀態變數 | 描述 |
|---|---|---|
block.timestamp | uint256 | 目前區塊時期的時間戳 |
msg.sender | address | 訊息發送者(目前調用) |
函數
用最簡單的術語來說,函數可以取得資訊或者設定資訊來回應傳入的交易。
有兩種函數調用方式:
internal– 這些不會建立 EVM 呼叫- 內部函數和狀態變數只能在內部存取 (即從目前合約或衍生自它的合約中存取)
external– 這些會建立 EVM 呼叫- 外部函數是合約介面的一部分,這表示可以從其他合約與透過交易調用。 外部函數
f無法在內部呼叫 (即f()無法運作,但this.f()可以)。
- 外部函數是合約介面的一部分,這表示可以從其他合約與透過交易調用。 外部函數
它們也可以是 public 或 private
public函數可以從合約內部或透過訊息從外部呼叫private函數只有在定義它們的合約中才可見,在衍生合約中則不可見
函數和狀態變數都可以被定義為 Public 或 Private
以下是更新合約狀態變數的函數:
1// Solidity 範例2function update_name(string value) public {3 dapp_name = value;4}- 類型為
string的參數value會傳遞至函數:update_name - 它被宣告為
public,代表任何人都可以存取 - 它未被宣告為
view,因此可以修改合約狀態
檢視函數
這些函數保證不會修改合約資料的狀態。 常見範例為「getter」函數,例如,你可能用此接收使用者的餘額。
1// Solidity 範例2function balanceOf(address _owner) public view returns (uint256 _balance) {3 return ownerPizzaCount[_owner];4}1dappName: public(string)23@view4@public5def readName() -> string:6 return dappName以下情況被視為修改狀態:
- 寫入狀態變數。
- 發出事件opens in a new tab。
- 建立其他合約opens in a new tab。
- 使用
selfdestruct。 - 透過調用傳送以太幣。
- 呼叫任何未標示為
view或pure的函數。 - 使用低階調用。
- 使用包含特定作業碼的行內組譯。
建構函數
constructor 函數只會在合約首次部署時執行一次。 與許多以類別為基礎的程式語言中的 constructor 類似,這些函數通常會將狀態變數初始化為其指定值。
1// Solidity 範例2// 初始化合約的資料,將 `owner`3// 設定為合約建立者的地址。4constructor() public {5 // 所有智能合約都依賴外部交易來觸發其函數。6 // `msg` 是一個全域變數,其中包含關於特定交易的相關資料,7 // 例如傳送者的地址以及交易中包含的 ETH 值。8 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/units-and-global-variables.html#block-and-transaction-properties9 owner = msg.sender;10}顯示全部1# Vyper 範例23@external4def __init__(_beneficiary: address, _bidding_time: uint256):5 self.beneficiary = _beneficiary6 self.auctionStart = block.timestamp7 self.auctionEnd = self.auctionStart + _bidding_time內建函數
除了自己合約上定義的變數與函數外,還有一些特殊的內建函數。 最明顯的例子:
address.send()– Soliditysend(address)– Vyper
這讓合約可以給其他帳戶傳送以太幣。
撰寫函數
你的函數需要:
- 參數變數及其類型(若接受參數)
- 聲明為 internal/external
- 聲明為 pure/view/payable
- 傳回類型(若傳回值)
1pragma solidity >=0.4.0 <=0.6.0;23contract ExampleDapp {4 string dapp_name; // 狀態變數56 // 在合約部署時呼叫,並初始化數值7 constructor() public {8 dapp_name = "My Example dapp";9 }1011 // Get 函數12 function read_name() public view returns(string) {13 return dapp_name;14 }1516 // Set 函數17 function update_name(string value) public {18 dapp_name = value;19 }20}顯示全部完整的合約看起來可能如上所示。 此處的 constructor 函數為 dapp_name 變數提供了一個初始值。
事件與日誌
事件讓你的智慧型合約能夠與你的前端或其他訂閱應用程式進行通訊。 一旦交易被驗證並新增到區塊中,智慧型合約就可以發出事件和記錄訊息,然後前端就能夠處理和利用這些資訊。
附註範例
以下是一些用 Solidity 編寫的範例。 如果你想試用這些程式碼,可以在 Remixopens in a new tab 中與其互動。
Hello world
1// 指定 Solidity 的版本,使用語意化版本控制。2// 深入了解:https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma3pragma solidity ^0.5.10;45// 定義一個名為 `HelloWorld` 的合約。6// 合約是函數和資料 (其狀態) 的集合。7// 部署之後,合約會存放在以太坊區塊鏈的特定地址上。8// 深入了解:https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html9contract HelloWorld {1011 // 宣告一個類型為 `string` 的狀態變數 `message`。12 // 狀態變數的值會永久儲存在合約儲存體中。13 // `public` 關鍵字讓變數可以從合約外部存取,14 // 並建立一個可供其他合約或用戶端呼叫以存取其值的函數。15 string public message;1617 // 與許多以類別為基礎的物件導向語言相似,建構函式是18 // 一個只在合約建立時執行的特殊函數。19 // 建構函式是用來初始化合約的資料。20 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors21 constructor(string memory initMessage) public {22 // 接受一個字串引數 `initMessage` 並將值設定23 // 到合約的 `message` 儲存變數中)。24 message = initMessage;25 }2627 // 一個公開函數,它接受一個字串引數28 // 並更新 `message` 儲存變數。29 function update(string memory newMessage) public {30 message = newMessage;31 }32}顯示全部代幣
1pragma solidity ^0.5.10;23contract Token {4 // `address` 類似於電子郵件地址,用來識別以太坊上的帳戶。5 // 地址可以代表一個智能合約或一個外部 (使用者) 帳戶。6 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/types.html#address7 address public owner;89 // `mapping` 本質上是一種雜湊表資料結構。10 // 這個 `mapping` 會將一個無正負號整數 (代幣餘額) 指派給一個地址 (代幣持有者)。11 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/types.html#mapping-types12 mapping (address => uint) public balances;1314 // 事件允許在區塊鏈上紀錄活動。15 // 以太坊用戶端可以監聽事件,以便對合約狀態變更做出反應。16 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#events17 event Transfer(address from, address to, uint amount);1819 // 初始化合約的資料,將 `owner`20 // 設定為合約建立者的地址。21 constructor() public {22 // 所有智能合約都依賴外部交易來觸發其函數。23 // `msg` 是一個全域變數,其中包含關於特定交易的相關資料,24 // 例如傳送者的地址以及交易中包含的 ETH 值。25 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/units-and-global-variables.html#block-and-transaction-properties26 owner = msg.sender;27 }2829 // 建立一定數量的新代幣並將其傳送到一個地址。30 function mint(address receiver, uint amount) public {31 // `require` 是一種控制結構,用於強制執行某些條件。32 // 如果 `require` 陳述式求值為 `false`,則會觸發例外,33 // 它會還原在目前呼叫期間對狀態所做的所有變更。34 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions3536 // 只有合約擁有者可以呼叫此函數37 require(msg.sender == owner, "您不是擁有者。");3839 // 強制執行代幣的最大數量40 require(amount < 1e60, "已超過最大發行量");4142 // 將 `receiver` 的餘額增加 `amount`43 balances[receiver] += amount;44 }4546 // 從任何呼叫者傳送一定數量的現有代幣到一個地址。47 function transfer(address receiver, uint amount) public {48 // 傳送者必須有足夠的代幣才能傳送49 require(amount <= balances[msg.sender], "餘額不足。");5051 // 調整兩個地址的代幣餘額52 balances[msg.sender] -= amount;53 balances[receiver] += amount;5455 // 發出先前定義的事件56 emit Transfer(msg.sender, receiver, amount);57 }58}顯示全部獨特的數位資產
1pragma solidity ^0.5.10;23// 從其他檔案將符號匯入到目前合約中。4// 在此案例中,是來自 OpenZeppelin 的一系列輔助合約。5// 深入了解:https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#importing-other-source-files67import "../node_modules/@openzeppelin/contracts/token/ERC721/IERC721.sol";8import "../node_modules/@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";9import "../node_modules/@openzeppelin/contracts/introspection/ERC165.sol";10import "../node_modules/@openzeppelin/contracts/math/SafeMath.sol";1112// `is` 關鍵字用於從外部合約繼承函數和關鍵字。13// 在此案例中,`CryptoPizza` 繼承自 `IERC721` 和 `ERC165` 合約。14// 深入了解:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#inheritance15contract CryptoPizza is IERC721, ERC165 {16 // 使用 OpenZeppelin 的 SafeMath 程式庫來安全地執行算術運算。17 // 深入了解:https://docs.openzeppelin.com/contracts/2.x/api/math#SafeMath18 using SafeMath for uint256;1920 // Solidity 中的常數狀態變數與其他語言相似,21 // 但您必須從一個在編譯時期為常數的運算式指派。22 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constant-state-variables23 uint256 constant dnaDigits = 10;24 uint256 constant dnaModulus = 10 ** dnaDigits;25 bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;2627 // Struct 類型讓您可以定義自己的類型28 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/types.html#structs29 struct Pizza {30 string name;31 uint256 dna;32 }3334 // 建立一個 Pizza 結構的空陣列35 Pizza[] public pizzas;3637 // 從 pizza ID 到其擁有者地址的對應38 mapping(uint256 => address) public pizzaToOwner;3940 // 從擁有者地址到所擁有代幣數量的對應41 mapping(address => uint256) public ownerPizzaCount;4243 // 從代幣 ID 到已核准地址的對應44 mapping(uint256 => address) pizzaApprovals;4546 // 您可以巢狀化對應,此範例將擁有者對應到操作員核准47 mapping(address => mapping(address => bool)) private operatorApprovals;4849 // 從字串 (名稱) 和 DNA 建立隨機 Pizza 的內部函數50 function _createPizza(string memory _name, uint256 _dna)51 // `internal` 關鍵字表示此函數僅在此合約52 // 和衍生自此合約的合約中可見53 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#visibility-and-getters54 internal55 // `isUnique` 是一個函數修飾詞,用於檢查 pizza 是否已存在56 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html#function-modifiers57 isUnique(_name, _dna)58 {59 // 將 Pizza 加入 Pizzas 陣列並取得 id60 uint256 id = SafeMath.sub(pizzas.push(Pizza(_name, _dna)), 1);6162 // 檢查 Pizza 擁有者是否與目前使用者相同63 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions6465 // 請注意 address(0) 是零地址,66 // 表示 pizza[id] 尚未分配給特定使用者。6768 assert(pizzaToOwner[id] == address(0));6970 // 將 Pizza 對應到擁有者71 pizzaToOwner[id] = msg.sender;72 ownerPizzaCount[msg.sender] = SafeMath.add(73 ownerPizzaCount[msg.sender],74 175 );76 }7778 // 從字串 (名稱) 建立隨機 Pizza79 function createRandomPizza(string memory _name) public {80 uint256 randDna = generateRandomDna(_name, msg.sender);81 _createPizza(_name, randDna);82 }8384 // 從字串 (名稱) 和擁有者 (建立者) 的地址產生隨機 DNA85 function generateRandomDna(string memory _str, address _owner)86 public87 // 標示為 `pure` 的函數保證不會讀取或修改狀態88 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#pure-functions89 pure90 returns (uint256)91 {92 // 從字串 (名稱) + 地址 (擁有者) 產生隨機 uint93 uint256 rand = uint256(keccak256(abi.encodePacked(_str))) +94 uint256(_owner);95 rand = rand % dnaModulus;96 return rand;97 }9899 // 傳回由擁有者找到的 Pizzas 陣列100 function getPizzasByOwner(address _owner)101 public102 // 標示為 `view` 的函數保證不會修改狀態103 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#view-functions104 view105 returns (uint256[] memory)106 {107 // 使用 `memory` 儲存位置來儲存僅在此函數呼叫108 // 的生命週期內有效的值。109 // 深入了解:https://solidity.readthedocs.io/en/v0.5.10/introduction-to-smart-contracts.html#storage-memory-and-the-stack110 uint256[] memory result = new uint256[](ownerPizzaCount[_owner]);111 uint256 counter = 0;112 for (uint256 i = 0; i < pizzas.length; i++) {113 if (pizzaToOwner[i] == _owner) {114 result[counter] = i;115 counter++;116 }117 }118 return result;119 }120121 // 將 Pizza 和所有權轉移到其他地址122 function transferFrom(address _from, address _to, uint256 _pizzaId) public {123 require(_from != address(0) && _to != address(0), "無效地址。");124 require(_exists(_pizzaId), "Pizza 不存在。");125 require(_from != _to, "無法轉移到相同地址。");126 require(_isApprovedOrOwner(msg.sender, _pizzaId), "地址未經核准。");127128 ownerPizzaCount[_to] = SafeMath.add(ownerPizzaCount[_to], 1);129 ownerPizzaCount[_from] = SafeMath.sub(ownerPizzaCount[_from], 1);130 pizzaToOwner[_pizzaId] = _to;131132 // 發出匯入的 IERC721 合約中定義的事件133 emit Transfer(_from, _to, _pizzaId);134 _clearApproval(_to, _pizzaId);135 }136137 /**138 * 安全地將指定代幣 ID 的所有權轉移到另一個地址139 * 如果目標地址是合約,則必須實作 `onERC721Received`,140 * 它會在安全轉移時呼叫,並傳回魔術值141 * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`;142 * 否則,轉移將被還原。143 */144 function safeTransferFrom(address from, address to, uint256 pizzaId)145 public146 {147 // solium-disable-next-line arg-overflow148 this.safeTransferFrom(from, to, pizzaId, "");149 }150151 /**152 * 安全地將指定代幣 ID 的所有權轉移到另一個地址153 * 如果目標地址是合約,則必須實作 `onERC721Received`,154 * 它會在安全轉移時呼叫,並傳回魔術值155 * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`;156 * 否則,轉移將被還原。157 */158 function safeTransferFrom(159 address from,160 address to,161 uint256 pizzaId,162 bytes memory _data163 ) public {164 this.transferFrom(from, to, pizzaId);165 require(_checkOnERC721Received(from, to, pizzaId, _data), "必須實作 onERC721Received。");166 }167168 /**169 * 在目標地址上叫用 `onERC721Received` 的內部函數170 * 如果目標地址不是合約,則不會執行此呼叫171 */172 function _checkOnERC721Received(173 address from,174 address to,175 uint256 pizzaId,176 bytes memory _data177 ) internal returns (bool) {178 if (!isContract(to)) {179 return true;180 }181182 bytes4 retval = IERC721Receiver(to).onERC721Received(183 msg.sender,184 from,185 pizzaId,186 _data187 );188 return (retval == _ERC721_RECEIVED);189 }190191 // 銷毀 Pizza - 完全摧毀代幣192 // `external` 函數修飾詞表示此函數是193 // 合約介面的一部分,其他合約可以呼叫它194 function burn(uint256 _pizzaId) external {195 require(msg.sender != address(0), "無效地址。");196 require(_exists(_pizzaId), "Pizza 不存在。");197 require(_isApprovedOrOwner(msg.sender, _pizzaId), "地址未經核准。");198199 ownerPizzaCount[msg.sender] = SafeMath.sub(200 ownerPizzaCount[msg.sender],201 1202 );203 pizzaToOwner[_pizzaId] = address(0);204 }205206 // 依地址傳回 Pizzas 的計數207 function balanceOf(address _owner) public view returns (uint256 _balance) {208 return ownerPizzaCount[_owner];209 }210211 // 依 id 傳回找到的 Pizza 的擁有者212 function ownerOf(uint256 _pizzaId) public view returns (address _owner) {213 address owner = pizzaToOwner[_pizzaId];214 require(owner != address(0), "無效的 Pizza ID。");215 return owner;216 }217218 // 核准其他地址轉移 Pizza 的所有權219 function approve(address _to, uint256 _pizzaId) public {220 require(msg.sender == pizzaToOwner[_pizzaId], "必須是 Pizza 擁有者。");221 pizzaApprovals[_pizzaId] = _to;222 emit Approval(msg.sender, _to, _pizzaId);223 }224225 // 傳回特定 Pizza 的已核准地址226 function getApproved(uint256 _pizzaId)227 public228 view229 returns (address operator)230 {231 require(_exists(_pizzaId), "Pizza 不存在。");232 return pizzaApprovals[_pizzaId];233 }234235 /**236 * 清除指定代幣 ID 目前核准的私有函數237 * 如果指定地址確實不是代幣的擁有者,則還原238 */239 function _clearApproval(address owner, uint256 _pizzaId) private {240 require(pizzaToOwner[_pizzaId] == owner, "必須是 pizza 擁有者。");241 require(_exists(_pizzaId), "Pizza 不存在。");242 if (pizzaApprovals[_pizzaId] != address(0)) {243 pizzaApprovals[_pizzaId] = address(0);244 }245 }246247 /*248 * 設定或取消設定指定操作員的核准249 * 操作員被允許代表傳送者轉移其所有代幣250 */251 function setApprovalForAll(address to, bool approved) public {252 require(to != msg.sender, "無法核准自己的地址");253 operatorApprovals[msg.sender][to] = approved;254 emit ApprovalForAll(msg.sender, to, approved);255 }256257 // 告知操作員是否已由指定擁有者核准258 function isApprovedForAll(address owner, address operator)259 public260 view261 returns (bool)262 {263 return operatorApprovals[owner][operator];264 }265266 // 取得 Pizza 的所有權 - 僅限已核准的使用者267 function takeOwnership(uint256 _pizzaId) public {268 require(_isApprovedOrOwner(msg.sender, _pizzaId), "地址未經核准。");269 address owner = this.ownerOf(_pizzaId);270 this.transferFrom(owner, msg.sender, _pizzaId);271 }272273 // 檢查 Pizza 是否存在274 function _exists(uint256 pizzaId) internal view returns (bool) {275 address owner = pizzaToOwner[pizzaId];276 return owner != address(0);277 }278279 // 檢查地址是否為擁有者或已獲准轉移 Pizza280 function _isApprovedOrOwner(address spender, uint256 pizzaId)281 internal282 view283 returns (bool)284 {285 address owner = pizzaToOwner[pizzaId];286 // 因下列問題停用 solium 檢查287 // https://github.com/duaraghav8/Solium/issues/175288 // solium-disable-next-line operator-whitespace289 return (spender == owner ||290 this.getApproved(pizzaId) == spender ||291 this.isApprovedForAll(owner, spender));292 }293294 // 檢查 Pizza 是否獨一無二且尚不存在295 modifier isUnique(string memory _name, uint256 _dna) {296 bool result = true;297 for (uint256 i = 0; i < pizzas.length; i++) {298 if (299 keccak256(abi.encodePacked(pizzas[i].name)) ==300 keccak256(abi.encodePacked(_name)) &&301 pizzas[i].dna == _dna302 ) {303 result = false;304 }305 }306 require(result, "具有此名稱的 Pizza 已存在。");307 _;308 }309310 // 傳回目標地址是否為合約311 function isContract(address account) internal view returns (bool) {312 uint256 size;313 // 目前沒有比檢查該地址的程式碼大小更好的方法來檢查地址中是否有合約。314 // 如需有關其運作方式的更多詳細資訊,請參閱 https://ethereum.stackexchange.com/a/14016/36603。315 // TODO 在 Serenity 發布前再次檢查,因為屆時所有地址都將是316 // 合約。317 // solium-disable-next-line security/no-inline-assembly318 assembly {319 size := extcodesize(account)320 }321 return size > 0;322 }323}顯示全部延伸閱讀
請參閱 Solidity 和 Vyper 文件,獲得智慧型合約更完整的概觀:
相關主題
相關教學
- 縮減合約以克服合約大小限制 – 一些縮減智能合約大小的實用技巧。
- 使用事件記錄智能合約的資料 – 智能合約事件簡介,以及如何使用它們來記錄資料。
- 從 Solidity 與其他合約互動 – 如何從現有合約部署智能合約並與其互動。