智慧型合約結構
最後編輯: @K0ue1(opens in a new tab), 2024年6月14日
智慧型合約是在以太坊地址運作的程式。 由可以在接收交易後執行的資料與函數組成。 此為智慧型合約組成的概覽。
基本資訊
務必先瞭解智慧型合約。 此文件假設你已熟悉 JavaScript 或 Python 等程式語言。
資料
任何合約資料都須指定至 storage
或 memory
這兩個位置。 修改智慧型合約的存儲很麻煩,所以必須謹慎思考要將資料儲存至何處。
存儲
永久資料也稱為存儲,並由狀態變數表示。 這些值會永久儲存於區塊鏈上。 你需要聲明一個類型,以便於合約在編譯時可以追蹤在區塊鏈上需要多少存儲空間。
1// Solidity 範例2contract SimpleStorage {3 uint storedData; //狀態變量4 // ...5}複製
1# Vyper 範例2storedData: int128複製
如果已編寫過物件導向程式語言,應該會熟悉大多數類型。 但如果剛接觸以太坊開發,則會不熟悉 address
類型。
一個 address
類型可以容納一個以太坊地址,相當於 20 個位元組或 160 個位元。 它會以十六進制的形式傳回,前綴是 0x。
其他類型包含:
- 布林值
- 整數
- 定點數
- 固定規模的位元組陣列
- 動態規模的位元組陣列
- 有理數和整數常值
- 字串常值
- 十六進位常值
- 列舉
如需更多說明,請參閱文件:
記憶體
僅在合約函數的執行生命週期儲存的值稱為記憶體變數。 由於這些變數不是永久儲存在區塊鏈上,所以使用成本要低得多。
在 Solidity 文件(opens in a new tab)中深入瞭解以太坊虛擬機如何儲存資料(存儲、記憶體和堆疊)。
環境變數
除了在自已的合約上定義的變數外,還有一些特殊的全域變數。 它們主要用於提供有關區塊鏈或目前交易的資訊。
範例:
屬性 | 狀態變數 | 描述 |
---|---|---|
block.timestamp | uint256 | 目前區塊時期的時間戳 |
msg.sender | address | 訊息發送者(目前調用) |
函數
用最簡單的術語來說,函數可以取得資訊或者設定資訊來回應傳入的交易。
有兩種函數調用方式:
Internal
– 不會建立以太坊虛擬機調用- 內部函數和狀態變數只能在內部存取(如在目前合約內部或從其衍生的合約存取)
External
– 會建立以太坊虛擬機調用- 外部函數是合約介面的一部分,這表示可以從其他合約與透過交易調用。 一個外部函數
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
函數只在首次部署時執行一次。 與許多基於類型之程式語言的 constructor
函數類似,這些函數常將狀態變數初始化為指定值。
1// Solidity 示例2// 初始化合約數據, 設置 `owner`為合約的創建者。3constructor() public {4 // 所有智慧型合約依賴外部交易來觸發其函數。5 // `msg` 是一個全局變量,包含了給定交易的相關數據,6 // 例如發送者的地址和交易中包含的ETH數量。7 // 了解更多: https://solidity.readthedocs.io/en/v0.5.10/units-and-global-variables.html#block-and-transaction-properties8 owner = msg.sender;9}顯示全部複製
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; //state variable56 /*在合約部署時調用以初始化數據*/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 編寫的範例。 若你想試著編寫程式碼,可以在 Remix(opens 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 // 一個public函數接受字符參數並更新存儲變量 `message`28 function update(string memory newMessage) public {29 message = newMessage;30 }31}顯示全部複製
代幣
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` 是一個哈希表(hash table)數據結構10 // 此 `mapping` 將一個無符號整數 (代幣餘額) 分配給地址 (代幣持有者)。11 // 了解更多: https://solidity.readthedocs.io/en/v0.5.10/types.html#mapping-types12 mapping (address => uint) public balances;1314// 事件(Events)允許在區塊鏈上記錄活動。15 // 以太坊客戶端可以監聽事件,以便對合約狀態更改作出反應。16 // 了解更多: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#events17 event Transfer(address from, address to, uint amount);1819 // 初始化合約數據,設置 `owner`為合約創建者的地址。20 constructor() public {21 // 所有智慧型合約依賴外部交易來觸發其函數。22 // `msg` 是一個全局變量,包含了給定交易的相關數據,23 // 例如發送者的地址和交易中包含的ETH數量。24 // 了解更多: https://solidity.readthedocs.io/en/v0.5.10/units-and-global-variables.html#block-and-transaction-properties25 owner = msg.sender;26 }2728 // 創建一些新代幣並發送給一個地址29 function mint(address receiver, uint amount) public {30 // `require` 是一個用於強制執行某些條件的控制結構。31 // 如果 `require` 的條件為 `false`, 則異常被觸發,32 // 所有在當前調用中對狀態的更改將被還原。33 // 了解更多: https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions3435 // 只有合約的擁有者可以調用這個函數36 require(msg.sender == owner, "You are not the owner.");3738 // 保證代幣的最大數量39 require(amount < 1e60, "Maximum issuance succeeded");4041 // 將 `receiver` 持有的代幣數量數量增加 `amount`42 balances[receiver] += amount;43 }4445 // 發送一定數量調用者的代幣給一個地址46 function transfer(address receiver, uint amount) public {47 // 發送者必須有足夠數量的代幣用於發送48 require(amount <= balances[msg.sender], "Insufficient balance.");4950 // 調整兩個帳戶的餘額51 balances[msg.sender] -= amount;52 balances[receiver] += amount;5354 // 觸發之前定義的事件。55 emit Transfer(msg.sender, receiver, amount);56 }57}顯示全部複製
獨特的數位資產
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's SafeMath 庫來安全執行算數操作。17 // 了解更多: https://docs.openzeppelin.com/contracts/2.x/api/math#SafeMath18 using SafeMath for uint256;1920 //Solidity語言中的常量(Constant)狀態變量與其他語言類似。21 // 但是必須用一個表達式為常量賦值,而這個表達式本身必須在編譯時是一個常量。22 // Learn more: 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 types let you define your own type28 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/types.html#structs29 struct Pizza {30 string name;31 uint256 dna;32 }3334 // Creates an empty array of Pizza structs35 Pizza[] public pizzas;3637 // Mapping from pizza ID to its owner's address38 mapping(uint256 => address) public pizzaToOwner;3940 // Mapping from owner's address to number of owned token41 mapping(address => uint256) public ownerPizzaCount;4243 // Mapping from token ID to approved address44 mapping(uint256 => address) pizzaApprovals;4546 // You can nest mappings, this example maps owner to operator approvals47 mapping(address => mapping(address => bool)) private operatorApprovals;4849 // Internal function to create a random Pizza from string (name) and DNA50 function _createPizza(string memory _name, uint256 _dna)51 // The `internal` keyword means this function is only visible52 // within this contract and contracts that derive this contract53 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#visibility-and-getters54 internal55 // `isUnique` is a function modifier that checks if the pizza already exists56 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html#function-modifiers57 isUnique(_name, _dna)58 {59 // Adds Pizza to array of Pizzas and get id60 uint256 id = SafeMath.sub(pizzas.push(Pizza(_name, _dna)), 1);6162 // Checks that Pizza owner is the same as current user63 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions6465 // note that address(0) is the zero address,66 // indicating that pizza[id] is not yet allocated to a particular user.6768 assert(pizzaToOwner[id] == address(0));6970 // Maps the Pizza to the owner71 pizzaToOwner[id] = msg.sender;72 ownerPizzaCount[msg.sender] = SafeMath.add(73 ownerPizzaCount[msg.sender],74 175 );76 }7778 // Creates a random Pizza from string (name)79 function createRandomPizza(string memory _name) public {80 uint256 randDna = generateRandomDna(_name, msg.sender);81 _createPizza(_name, randDna);82 }8384 // Generates random DNA from string (name) and address of the owner (creator)85 function generateRandomDna(string memory _str, address _owner)86 public87 // Functions marked as `pure` promise not to read from or modify the state88 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#pure-functions89 pure90 returns (uint256)91 {92 // Generates random uint from string (name) + address (owner)93 uint256 rand = uint256(keccak256(abi.encodePacked(_str))) +94 uint256(_owner);95 rand = rand % dnaModulus;96 return rand;97 }9899 // Returns array of Pizzas found by owner100 function getPizzasByOwner(address _owner)101 public102 // Functions marked as `view` promise not to modify state103 // Learn more: https://solidity.readthedocs.io/en/v0.5.10/contracts.html#view-functions104 view105 returns (uint256[] memory)106 {107 // Uses the `memory` storage location to store values only for the108 // lifecycle of this function call.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), "Invalid address.");124 require(_exists(_pizzaId), "Pizza does not exist.");125 require(_from != _to, "Cannot transfer to the same address.");126 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");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 * 該函數調用了安全轉帳並且返回一個magic value。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 * 該函數調用安全轉帳並返回一個magic value155 * `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), "Must implement onERC721Received.");166 }167168 /**169 * Internal function to invoke `onERC721Received` on a target address170 * The call is not executed if the target address is not a contract171 */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 // Burns a Pizza - destroys Token completely192 // The `external` function modifier means this function is193 // part of the contract interface and other contracts can call it194 function burn(uint256 _pizzaId) external {195 require(msg.sender != address(0), "Invalid address.");196 require(_exists(_pizzaId), "Pizza does not exist.");197 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");198199 ownerPizzaCount[msg.sender] = SafeMath.sub(200 ownerPizzaCount[msg.sender],201 1202 );203 pizzaToOwner[_pizzaId] = address(0);204 }205206 // Returns count of Pizzas by address207 function balanceOf(address _owner) public view returns (uint256 _balance) {208 return ownerPizzaCount[_owner];209 }210211 // Returns owner of the Pizza found by id212 function ownerOf(uint256 _pizzaId) public view returns (address _owner) {213 address owner = pizzaToOwner[_pizzaId];214 require(owner != address(0), "Invalid Pizza ID.");215 return owner;216 }217218 // Approves other address to transfer ownership of Pizza219 function approve(address _to, uint256 _pizzaId) public {220 require(msg.sender == pizzaToOwner[_pizzaId], "Must be the Pizza owner.");221 pizzaApprovals[_pizzaId] = _to;222 emit Approval(msg.sender, _to, _pizzaId);223 }224225 // Returns approved address for specific Pizza226 function getApproved(uint256 _pizzaId)227 public228 view229 returns (address operator)230 {231 require(_exists(_pizzaId), "Pizza does not exist.");232 return pizzaApprovals[_pizzaId];233 }234235 /**236 * Private function to clear current approval of a given token ID237 * Reverts if the given address is not indeed the owner of the token238 */239 function _clearApproval(address owner, uint256 _pizzaId) private {240 require(pizzaToOwner[_pizzaId] == owner, "Must be pizza owner.");241 require(_exists(_pizzaId), "Pizza does not exist.");242 if (pizzaApprovals[_pizzaId] != address(0)) {243 pizzaApprovals[_pizzaId] = address(0);244 }245 }246247 /*248 * Sets or unsets the approval of a given operator249 * An operator is allowed to transfer all tokens of the sender on their behalf250 */251 function setApprovalForAll(address to, bool approved) public {252 require(to != msg.sender, "Cannot approve own address");253 operatorApprovals[msg.sender][to] = approved;254 emit ApprovalForAll(msg.sender, to, approved);255 }256257 // Tells whether an operator is approved by a given owner258 function isApprovedForAll(address owner, address operator)259 public260 view261 returns (bool)262 {263 return operatorApprovals[owner][operator];264 }265266 // Takes ownership of Pizza - only for approved users267 function takeOwnership(uint256 _pizzaId) public {268 require(_isApprovedOrOwner(msg.sender, _pizzaId), "Address is not approved.");269 address owner = this.ownerOf(_pizzaId);270 this.transferFrom(owner, msg.sender, _pizzaId);271 }272273 // Checks if Pizza exists274 function _exists(uint256 pizzaId) internal view returns (bool) {275 address owner = pizzaToOwner[pizzaId];276 return owner != address(0);277 }278279 // Checks if address is owner or is approved to transfer Pizza280 function _isApprovedOrOwner(address spender, uint256 pizzaId)281 internal282 view283 returns (bool)284 {285 address owner = pizzaToOwner[pizzaId];286 // Disable solium check because of287 // 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 // Check if Pizza is unique and doesn't exist yet295 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 with such name already exists.");307 _;308 }309310 // Returns whether the target address is a contract311 function isContract(address account) internal view returns (bool) {312 uint256 size;313 // Currently there is no better way to check if there is a contract in an address314 // than to check the size of the code at that address.315 // 參閱 https://ethereum.stackexchange.com/a/14016/36603316 // 了解更多信息.317 // TODO: 在Serenity發布前再次檢查這裡,318 // 否則到時所有地址都將判斷為合約.319 // solium-disable-next-line security/no-inline-assembly320 assembly {321 size := extcodesize(account)322 }323 return size > 0;324 }325}顯示全部複製
衍生閱讀
請參閱 Solidity 和 Vyper 文件,獲得智慧型合約更完整的概觀:
相關主題
相關教程
- 縮減合約大小應對合約大小限制 – 減少智慧型合約大小的實用秘訣。
- 用事件記錄智慧型合約資料 – 對智慧型合約事件進行介紹,以及如何使用事件來記錄資料。
- 與其他 Solidity 合約互動 – 如何從現有合約部署智慧型合約並與之互動。