跳转至主要内容
Change page

详解智能合约

页面最后更新: 2025年12月2日

智能合约是一种在以太坊某个地址上运行的程序。 它们是由数据和函数组成的,可以在收到交易时执行。 以下概述一个智能合约的组成。

前提条件

请先确保您已阅读智能合约的相关信息。 本文档假设你已经熟悉某种编程语言,例如 JavaScript 或 Python。

数据

任何合约数据都必须分配到一个位置:storagememory。 在智能合约中修改存储消耗很大,因此你需要考虑数据在哪里存取。

存储

持久性数据被称之为存储,由状态变量表示。 这些值被永久地存储在区块链上。 你需要声明一个类型,以便于合约在编译时可以跟踪它在区块链上需要多少存储。

1// Solidity 示例
2contract SimpleStorage {
3 uint storedData; // 状态变量
4 // ...
5}
1# Vyper example
2storedData: int128

如果用过面向对象编程语言,应该会熟悉大多数类型。 但如果是刚接触以太坊开发,则会发现 address 是一个新类型。

address 类型可以容纳一个以太坊地址,相当于 20 字节或 160 位。 它以十六进制的形式返回,前导是 0x。

其它类型包括:

  • 布尔
  • 整数(integer)
  • 定点数(fixed point numbers)
  • 固定大小的字节数组(fixed-size byte arrays)
  • 动态大小字节数组
  • 有理数和整数字面量
  • 字符串字面量
  • 十六进制字面量
  • 枚举

了解更多信息,请参阅文档:

内存

仅在合约函数执行期间存储的值被称为内存变量。 由于这些变量不是永久地存储在区块链上,所以它们的使用成本要低得多。

Solidity 文档opens in a new tab中了解更多关于 EVM 如何存储数据(存储、内存和堆栈)的信息。

环境变量

除了在自己合约上定义的变量之外,还有一些特殊的全局变量。 它们主要用于提供有关区块链或当前交易的信息。

例子:

属性状态变量描述
区块时间戳uint256当前区块的时间戳
发送者地址消息的发送者(当前调用)

函数

用最简单的术语来说,函数可以获得信息或设置信息,以响应传入的交易。

有两种函数调用方式:

  • internal – 不会创建 EVM 调用
    • 内部函数和状态变量只能在内部访问(即从当前合约或其派生合约中访问)
  • external – 会创建 EVM 调用
    • External 函数是合约接口的一部分,这意味着他可以被其它合约和交易调用。 外部函数 f 不能在内部调用(即 f() 不起作用,但 this.f() 可以)。

它们也可以是 publicprivate

  • public 函数可以从合约内部调用,也可以通过消息从外部调用
  • private 函数仅在定义它们的合约中可见,在其派生合约中不可见

函数和状态变量都可以被定义为 public 或 private

下面是更新合约上一个状态变量的函数:

1// Solidity example
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)
2
3@view
4@public
5def readName() -> string:
6 return dappName

这些操作被视为修改状态:

  1. 写入状态变量。
  2. 触发事件opens in a new tab
  3. 创建其他合约opens in a new tab
  4. 使用 selfdestruct
  5. 通过调用发送 ether。
  6. 调用任何未标记 view 或 pure 的函数。
  7. 使用底层调用。
  8. 使用包含某些操作码的内联程序组。

构造函数

constructor 函数仅在首次部署合约时执行一次。 与许多基于类的编程语言中的 constructor 一样,这些函数通常会将状态变量初始化为其指定值。

1// Solidity 示例
2// 初始化合约的数据,将“所有者”
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-properties
9 owner = msg.sender;
10}
显示全部
1# Vyper 示例
2
3@external
4def __init__(_beneficiary: address, _bidding_time: uint256):
5 self.beneficiary = _beneficiary
6 self.auctionStart = block.timestamp
7 self.auctionEnd = self.auctionStart + _bidding_time

内置函数

除了自己在合约中定义的变量和函数外,还有一些特殊的内置函数。 最明显的例子是:

  • address.send() – Solidity
  • send(address) – Vyper

这使合约可以发送以太币给其它帐户。

编写函数

你的函数需要:

  • 参数变量及其类型(如果它接受参数)
  • 声明为 internal/external
  • 声明为 pure/view/payable
  • 返回类型(如果它返回值)
1pragma solidity >=0.4.0 <=0.6.0;
2
3contract ExampleDapp {
4 string dapp_name; // 状态变量
5
6 // 在部署合约并初始化其值时调用
7 constructor() public {
8 dapp_name = "我的示例 dapp";
9 }
10
11 // Get 函数
12 function read_name() public view returns(string) {
13 return dapp_name;
14 }
15
16 // 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#pragma
3pragma solidity ^0.5.10;
4
5// 定义一个名为 `HelloWorld` 的合约。
6// 合约是函数和数据(其状态)的集合。
7// 一旦部署,合约便会存在于以太坊区块链上的一个特定地址。
8// 了解更多:https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html
9contract HelloWorld {
10
11 // 声明一个类型为 `string` 的状态变量 `message`。
12 // 状态变量是其值被永久存储在合约存储中的变量。
13 // 关键字 `public` 使变量可以从合约外部访问
14 // 并创建一个其他合约或客户端可以调用以访问该值的函数。
15 string public message;
16
17 // 与许多基于类的面向对象语言类似,构造函数是
18 // 一个仅在创建合约时执行的特殊函数。
19 // 构造函数用于初始化合约的数据。
20 // 了解更多:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors
21 constructor(string memory initMessage) public {
22 // 接受一个字符串参数 `initMessage` 并将该值设置
23 // 到合约的 `message` 存储变量中)。
24 message = initMessage;
25 }
26
27 // 一个接受字符串参数并更新 `message` 存储变量的
28 // public 函数。
29 function update(string memory newMessage) public {
30 message = newMessage;
31 }
32}
显示全部

代币

1pragma solidity ^0.5.10;
2
3contract Token {
4 // `address` 类似于电子邮件地址 - 用于在以太坊上标识帐户。
5 // 地址可以表示智能合约或外部(用户)帐户。
6 // 了解更多:https://solidity.readthedocs.io/en/v0.5.10/types.html#address
7 address public owner;
8
9 // `mapping` 本质上是一个哈希表数据结构。
10 // 此 `mapping` 将一个无符号整数(代币余额)分配给一个地址(代币持有者)。
11 // 了解更多:https://solidity.readthedocs.io/en/v0.5.10/types.html#mapping-types
12 mapping (address => uint) public balances;
13
14 // 事件允许在区块链上记录活动日志。
15 // 以太坊客户端可以侦听事件,以便对合约状态更改做出反应。
16 // 了解更多:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#events
17 event Transfer(address from, address to, uint amount);
18
19 // 初始化合约数据,将“所有者”
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-properties
26 owner = msg.sender;
27 }
28
29 // 创建一定数量的新代币并将其发送到一个地址。
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-exceptions
35
36 // 只有合约所有者可以调用此函数
37 require(msg.sender == owner, "你不是所有者。");
38
39 // 强制执行代币的最大数量
40 require(amount < 1e60, "已超出最大发行量");
41
42 // 将 `receiver` 的余额增加 `amount`
43 balances[receiver] += amount;
44 }
45
46 // 从任何调用者向一个地址发送一定数量的现有代币。
47 function transfer(address receiver, uint amount) public {
48 // 发送者必须有足够的代币才能发送
49 require(amount <= balances[msg.sender], "余额不足。");
50
51 // 调整两个地址的代币余额
52 balances[msg.sender] -= amount;
53 balances[receiver] += amount;
54
55 // 触发之前定义的事件
56 emit Transfer(msg.sender, receiver, amount);
57 }
58}
显示全部

独特的数字资产

1pragma solidity ^0.5.10;
2
3// 将其他文件中的符号导入当前合约。
4// 在本例中,为来自 OpenZeppelin 的一系列辅助合约。
5// 了解更多:https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#importing-other-source-files
6
7import "../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";
11
12// `is` 关键字用于从外部合约继承函数和关键字。
13// 在本例中,`CryptoPizza` 继承自 `IERC721` 和 `ERC165` 合约。
14// 了解更多:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#inheritance
15contract CryptoPizza is IERC721, ERC165 {
16 // 使用 OpenZeppelin 的 SafeMath 库安全地执行算术运算。
17 // 了解更多:https://docs.openzeppelin.com/contracts/2.x/api/math#SafeMath
18 using SafeMath for uint256;
19
20 // Solidity 中的常量状态变量与其他语言类似
21 // 但你必须从编译时为常量的表达式中赋值。
22 // 了解更多:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constant-state-variables
23 uint256 constant dnaDigits = 10;
24 uint256 constant dnaModulus = 10 ** dnaDigits;
25 bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;
26
27 // 结构类型允许你定义自己的类型
28 // 了解更多:https://solidity.readthedocs.io/en/v0.5.10/types.html#structs
29 struct Pizza {
30 string name;
31 uint256 dna;
32 }
33
34 // 创建一个空的 Pizza 结构数组
35 Pizza[] public pizzas;
36
37 // 从披萨 ID 到其所有者地址的映射
38 mapping(uint256 => address) public pizzaToOwner;
39
40 // 从所有者地址到所拥有代币数量的映射
41 mapping(address => uint256) public ownerPizzaCount;
42
43 // 从代币 ID 到批准地址的映射
44 mapping(uint256 => address) pizzaApprovals;
45
46 // 你可以嵌套映射,此示例将所有者映射到操作员批准
47 mapping(address => mapping(address => bool)) private operatorApprovals;
48
49 // 从字符串(名称)和 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-getters
54 internal
55 // `isUnique` 是一个函数修饰符,用于检查披萨是否已存在
56 // 了解更多:https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html#function-modifiers
57 isUnique(_name, _dna)
58 {
59 // 将 Pizza 添加到 Pizzas 数组并获取 id
60 uint256 id = SafeMath.sub(pizzas.push(Pizza(_name, _dna)), 1);
61
62 // 检查 Pizza 所有者是否与当前用户相同
63 // 了解更多:https://solidity.readthedocs.io/en/v0.5.10/control-structures.html#error-handling-assert-require-revert-and-exceptions
64
65 // 请注意,address(0) 是零地址,
66 // 表示 pizza[id] 尚未分配给特定用户。
67
68 assert(pizzaToOwner[id] == address(0));
69
70 // 将 Pizza 映射到所有者
71 pizzaToOwner[id] = msg.sender;
72 ownerPizzaCount[msg.sender] = SafeMath.add(
73 ownerPizzaCount[msg.sender],
74 1
75 );
76 }
77
78 // 从字符串(名称)创建随机 Pizza
79 function createRandomPizza(string memory _name) public {
80 uint256 randDna = generateRandomDna(_name, msg.sender);
81 _createPizza(_name, randDna);
82 }
83
84 // 从字符串(名称)和所有者(创建者)的地址生成随机 DNA
85 function generateRandomDna(string memory _str, address _owner)
86 public
87 // 标记为 `pure` 的函数保证不会读取或修改状态
88 // 了解更多:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#pure-functions
89 pure
90 returns (uint256)
91 {
92 // 从字符串(名称)+ 地址(所有者)生成随机 uint
93 uint256 rand = uint256(keccak256(abi.encodePacked(_str))) +
94 uint256(_owner);
95 rand = rand % dnaModulus;
96 return rand;
97 }
98
99 // 返回按所有者找到的 Pizzas 数组
100 function getPizzasByOwner(address _owner)
101 public
102 // 标记为 `view` 的函数保证不会修改状态
103 // 了解更多:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#view-functions
104 view
105 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-stack
110 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 }
120
121 // 将 Pizza 和所有权转移到其他地址
122 function transferFrom(address _from, address _to, uint256 _pizzaId) public {
123 require(_from != address(0) && _to != address(0), "无效地址。");
124 require(_exists(_pizzaId), "披萨不存在。");
125 require(_from != _to, "不能转移到相同地址。");
126 require(_isApprovedOrOwner(msg.sender, _pizzaId), "地址未经批准。");
127
128 ownerPizzaCount[_to] = SafeMath.add(ownerPizzaCount[_to], 1);
129 ownerPizzaCount[_from] = SafeMath.sub(ownerPizzaCount[_from], 1);
130 pizzaToOwner[_pizzaId] = _to;
131
132 // 触发导入的 IERC721 合约中定义的事件
133 emit Transfer(_from, _to, _pizzaId);
134 _clearApproval(_to, _pizzaId);
135 }
136
137 /**
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 public
146 {
147 // solium-disable-next-line arg-overflow
148 this.safeTransferFrom(from, to, pizzaId, "");
149 }
150
151 /**
152 * 将给定代币 ID 的所有权安全地转移到另一个地址
153 * 如果目标地址是合约,则它必须实现 `onERC721Received`,
154 * 它在安全转移时被调用,并返回 magic value
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 _data
163 ) public {
164 this.transferFrom(from, to, pizzaId);
165 require(_checkOnERC721Received(from, to, pizzaId, _data), "必须实现 onERC721Received。");
166 }
167
168 /**
169 * 在目标地址上调用 `onERC721Received` 的内部函数
170 * 如果目标地址不是合约,则不执行调用
171 */
172 function _checkOnERC721Received(
173 address from,
174 address to,
175 uint256 pizzaId,
176 bytes memory _data
177 ) internal returns (bool) {
178 if (!isContract(to)) {
179 return true;
180 }
181
182 bytes4 retval = IERC721Receiver(to).onERC721Received(
183 msg.sender,
184 from,
185 pizzaId,
186 _data
187 );
188 return (retval == _ERC721_RECEIVED);
189 }
190
191 // 销毁一个 Pizza - 完全销毁代币
192 // `external` 函数修饰符表示此函数是
193 // 合约接口的一部分,其他合约可以调用它
194 function burn(uint256 _pizzaId) external {
195 require(msg.sender != address(0), "无效地址。");
196 require(_exists(_pizzaId), "披萨不存在。");
197 require(_isApprovedOrOwner(msg.sender, _pizzaId), "地址未经批准。");
198
199 ownerPizzaCount[msg.sender] = SafeMath.sub(
200 ownerPizzaCount[msg.sender],
201 1
202 );
203 pizzaToOwner[_pizzaId] = address(0);
204 }
205
206 // 按地址返回 Pizzas 的数量
207 function balanceOf(address _owner) public view returns (uint256 _balance) {
208 return ownerPizzaCount[_owner];
209 }
210
211 // 按 id 返回找到的 Pizza 的所有者
212 function ownerOf(uint256 _pizzaId) public view returns (address _owner) {
213 address owner = pizzaToOwner[_pizzaId];
214 require(owner != address(0), "无效披萨 ID。");
215 return owner;
216 }
217
218 // 批准其他地址转移 Pizza 的所有权
219 function approve(address _to, uint256 _pizzaId) public {
220 require(msg.sender == pizzaToOwner[_pizzaId], "必须是披萨所有者。");
221 pizzaApprovals[_pizzaId] = _to;
222 emit Approval(msg.sender, _to, _pizzaId);
223 }
224
225 // 返回特定 Pizza 的批准地址
226 function getApproved(uint256 _pizzaId)
227 public
228 view
229 returns (address operator)
230 {
231 require(_exists(_pizzaId), "披萨不存在。");
232 return pizzaApprovals[_pizzaId];
233 }
234
235 /**
236 * 清除给定代币 ID 的当前批准的私有函数
237 * 如果给定地址不是代币的真正所有者,则还原
238 */
239 function _clearApproval(address owner, uint256 _pizzaId) private {
240 require(pizzaToOwner[_pizzaId] == owner, "必须是披萨所有者。");
241 require(_exists(_pizzaId), "披萨不存在。");
242 if (pizzaApprovals[_pizzaId] != address(0)) {
243 pizzaApprovals[_pizzaId] = address(0);
244 }
245 }
246
247 /*
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 }
256
257 // 告知操作员是否获得给定所有者的批准
258 function isApprovedForAll(address owner, address operator)
259 public
260 view
261 returns (bool)
262 {
263 return operatorApprovals[owner][operator];
264 }
265
266 // 取得 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 }
272
273 // 检查 Pizza 是否存在
274 function _exists(uint256 pizzaId) internal view returns (bool) {
275 address owner = pizzaToOwner[pizzaId];
276 return owner != address(0);
277 }
278
279 // 检查地址是否为所有者或被批准转移 Pizza
280 function _isApprovedOrOwner(address spender, uint256 pizzaId)
281 internal
282 view
283 returns (bool)
284 {
285 address owner = pizzaToOwner[pizzaId];
286 // Disable solium check because of
287 // https://github.com/duaraghav8/Solium/issues/175
288 // solium-disable-next-line operator-whitespace
289 return (spender == owner ||
290 this.getApproved(pizzaId) == spender ||
291 this.isApprovedForAll(owner, spender));
292 }
293
294 // 检查 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 == _dna
302 ) {
303 result = false;
304 }
305 }
306 require(result, "具有此类名称的披萨已存在。");
307 _;
308 }
309
310 // 返回目标地址是否是合约
311 function isContract(address account) internal view returns (bool) {
312 uint256 size;
313 // 目前,没有比检查地址处的代码大小更好的方法来检查地址中是否存在合约。
314 // 有关其工作原理的更多详细信息,
315 // 请参阅 https://ethereum.stackexchange.com/a/14016/36603。
316 // TODO 在 Serenity 发布之前再次检查,因为届时所有地址都将是
317 // 合约。
318 // solium-disable-next-line security/no-inline-assembly
319 assembly {
320 size := extcodesize(account)
321 }
322 return size > 0;
323 }
324}
显示全部

扩展阅读{#further-reading}

查阅 Solidity 和 Vyper 文档,以获得关于智能合约的更完整概述:

本文对你有帮助吗?