详解智能合约
上次修改时间: @zhangyuenet(opens in a new tab), 2024年6月14日
智能合约是一种在以太坊某个地址上运行的程序。 它们是由数据和函数组成的,可以在收到交易时执行。 以下概述一个智能合约的组成。
前提条件
确保你已经先阅读了智能合约。 本文档假设你已经熟悉某种编程语言,例如 JavaScript 或 Python。
数据
任何合约数据必须分配到一个位置:要么是存储
,要么是内存
。 在智能合约中修改存储消耗很大,因此你需要考虑数据在哪里存取。
存储
持久性数据被称之为存储,由状态变量表示。 这些值被永久地存储在区块链上。 你需要声明一个类型,以便于合约在编译时可以跟踪它在区块链上需要多少存储。
1// Solidity example2contract SimpleStorage {3 uint storedData; // State variable4 // ...5}复制
1# Vyper example2storedData: int128复制
如果用过面向对象编程语言,应该会熟悉大多数类型。 但如果是刚接触以太坊开发,则会发现 address
是一个新类型。
一个 address
类型可以容纳一个以太坊地址,相当于 20 个字节或 160 位。 它以十六进制的形式返回,前导是 0x。
其它类型包括:
- 布尔
- 整数(integer)
- 定点数(fixed point numbers)
- 固定大小的字节数组(fixed-size byte arrays)
- 动态大小的字节数组(dynamically-sized byte arrays)
- 有理数和整数常量(Rational and integer literals)
- 字符常量(String literals)
- 十六进制常量(Hexadecimal literals)
- 枚举(Enums)
了解更多信息,请参阅文档:
内存
仅在合约函数执行期间存储的值被称为内存变量。 由于这些变量不是永久地存储在区块链上,所以它们的使用成本要低得多。
在 Solidity 文档(opens in a new tab)中了解更多关于以太坊虚拟机如何存储数据(存储、内存和栈)。
环境变量
除了在自己合约上定义的变量之外,还有一些特殊的全局变量。 它们主要用于提供有关区块链或当前交易的信息。
示例:
属性 | 状态变量 | 描述 |
---|---|---|
block.timestamp | uint256 | 当前区块的时间戳 |
msg.sender | 地址 | 消息的发送者(当前调用) |
函数
用最简单的术语来说,函数可以获得信息或设置信息,以响应传入的交易。
有两种函数调用方式:
internal
– 不会创建以太坊虚拟机调用- Internal 函数和状态变量只能在内部访问(只能在合约内部或者从其继承的合约内部访问)。
external
– 会创建以太坊虚拟机调用- External 函数是合约接口的一部分,这意味着他可以被其它合约和交易调用。 一个 external 函数
f
不可以被内部调用(即f()
不行,但this.f()
可以)。
- External 函数是合约接口的一部分,这意味着他可以被其它合约和交易调用。 一个 external 函数
它们可以是 public
或 private
public
函数可以在合约内部调用或者通过消息在合约外部调用private
函数仅在其被定义的合约内部可见,并且在该合约的派生合约中不可见。
函数和状态变量都可以被定义为 public 或 private
下面是更新合约上一个状态变量的函数:
1// Solidity example2function 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
。 - 通过调用发送 ether。
- 调用任何未标记为
view
或pure
的函数。 - 使用底层调用。
- 使用包含某些操作码的内联程序组。
构造函数
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 // Called when the contract is deployed and initializes the value7 constructor() public {8 dapp_name = "My Example dapp";9 }1011 // Get Function12 function read_name() public view returns(string) {13 return dapp_name;14 }1516 // Set Function17 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// Specifies the version of Solidity, using semantic versioning.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` 是一个哈希表数据结构。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 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 exceeded");4041 // 将 "收款人"的余额增加"金额"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 语言中的常量状态变量与其他语言类似。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 // See https://ethereum.stackexchange.com/a/14016/36603316 // for more details about how this works.317 // TODO Check this again before the Serenity release, because all addresses will be318 // contracts then.319 // solium-disable-next-line security/no-inline-assembly320 assembly {321 size := extcodesize(account)322 }323 return size > 0;324 }325}显示全部复制
延伸阅读
查阅 Solidity 和 Vyper 文档,以获得关于智能合约的更完整概述:
相关主题
相关教程
- 减少合约大小以应对合约大小的限制 – 一些减少智能合约大小的实用提示。
- 用事件记录智能合约的数据 ——对智能合约事件的介绍以及如何使用它们来记录数据。
- 在 Solidity 中与其它合约交互 ——如何从现有合约中部署智能合约并与之交互。