详解智能合约
页面最后更新: 2025年12月2日
智能合约是一种在以太坊某个地址上运行的程序。 它们是由数据和函数组成的,可以在收到交易时执行。 以下概述一个智能合约的组成。
前提条件
请先确保您已阅读智能合约的相关信息。 本文档假设你已经熟悉某种编程语言,例如 JavaScript 或 Python。
数据
任何合约数据都必须分配到一个位置:storage 或 memory。 在智能合约中修改存储消耗很大,因此你需要考虑数据在哪里存取。
存储
持久性数据被称之为存储,由状态变量表示。 这些值被永久地存储在区块链上。 你需要声明一个类型,以便于合约在编译时可以跟踪它在区块链上需要多少存储。
1// Solidity 示例2contract SimpleStorage {3 uint storedData; // 状态变量4 // ...5}1# Vyper example2storedData: 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()可以)。
- 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// 初始化合约的数据,将“所有者”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 = "我的示例 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 // 一个接受字符串参数并更新 `message` 存储变量的28 // public 函数。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 // 初始化合约数据,将“所有者”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 // 结构类型允许你定义自己的类型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 // 从披萨 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` 是一个函数修饰符,用于检查披萨是否已存在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), "披萨不存在。");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 * 它在安全转移时被调用,并返回 magic value141 * `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), "必须实现 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), "披萨不存在。");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), "无效披萨 ID。");215 return owner;216 }217218 // 批准其他地址转移 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 }224225 // 返回特定 Pizza 的批准地址226 function getApproved(uint256 _pizzaId)227 public228 view229 returns (address operator)230 {231 require(_exists(_pizzaId), "披萨不存在。");232 return pizzaApprovals[_pizzaId];233 }234235 /**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 }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 // 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 // 检查 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, "具有此类名称的披萨已存在。");307 _;308 }309310 // 返回目标地址是否是合约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-assembly319 assembly {320 size := extcodesize(account)321 }322 return size > 0;323 }324}显示全部扩展阅读{#further-reading}
查阅 Solidity 和 Vyper 文档,以获得关于智能合约的更完整概述:
相关话题
相关教程
- 缩减合约以对抗合约大小限制– 一些减小智能合约大小的实用技巧。
- 使用事件记录智能合约中的数据– 智能合约事件简介以及如何使用它们来记录数据。
- 在 Solidity 中与其他合约交互– 如何从现有合约部署智能合约并与之交互。