当前位置: 首页 > news >正文

【区块链安全 | 第十篇】智能合约概述

部分内容与前文互补。

在这里插入图片描述

文章目录

    • 一个简单的智能合约
    • 子货币(Subcurrency)示例
    • 区块链基础
      • 交易
      • 区块
      • 预编译合约

一个简单的智能合约

我们从一个基础示例开始,该示例用于设置变量的值,并允许其他合约访问它。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

代码的第一行表明,该源代码采用 GPL 3.0 许可证进行授权。在以源代码公开为默认规则的环境中,使用机器可读的许可证标识符是非常重要的。

接下来一行 pragma solidity >=0.4.16 <0.9.0; 指定了该合约适用于 Solidity 0.4.16 及以上版本,但不包括 0.9.0。这是为了确保合约不会在未来的破坏性更新(Breaking Changes)中出现兼容性问题。Pragma 语句是编译器的指令,类似于 C/C++ 语言中的 pragma once,用于指定源代码的编译方式。

在 Solidity 语言中,合约(contract) 本质上是一个代码(函数)和数据(状态)的集合,它们驻留在以太坊区块链上的特定地址处。

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

在合约 SimpleStorage 中,uint storedData; 声明了一个状态变量 storedData,其类型为 uint(无符号整数,默认为 256 位)。你可以把它看作数据库中的一个单一存储槽位,可以通过调用合约中的函数来查询和修改它。在这个示例中,合约提供了 set 和 get 两个函数,分别用于修改和获取 storedData 的值。

在 Solidity 中,访问当前合约的成员变量(如 storedData),通常无需使用 this. 前缀,直接使用变量名即可。这不仅仅是代码风格的问题,而是影响访问方式的关键区别(后续会详细讲解)。

这个合约本身功能还比较简单,但得益于以太坊的基础架构,它允许任何人存储一个数值,并让全球范围内的任何人访问。理论上,没有任何方法可以阻止你发布这个数值。但需要注意,任何人都可以再次调用 set 方法,修改存储的值,并覆盖之前的数据。不过,之前存储的数据仍然会保留在区块链的历史记录中。

后续会介绍如何实现访问权限控制,以便只有你自己才能修改这个值。

警告:使用 Unicode 文本时需要小心,因为一些看起来相似甚至完全相同的字符,可能具有不同的代码点(Code Point),因此它们的字节编码可能不同,从而引发安全或兼容性问题。
注意:所有标识符(包括合约名、函数名和变量名)都必须使用 ASCII 字符集。不过,你仍然可以在 string 类型的变量中存储 UTF-8 编码的数据。

子货币(Subcurrency)示例

以下合约实现了最简单形式的加密货币。该合约仅允许其创建者铸造新币。任何人都可以在没有用户名和密码的情况下相互转账,所需的只是一个以太坊密钥对。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.26;

// 该合约只能通过 IR 编译
contract Coin {
    // 关键字 "public" 使变量可被其他合约访问
    // 相当于所有人可见创建者的合约地址
    address public minter;
    mapping(address => uint) public balances;

    // 事件允许客户端对你声明的特定合约更改做出反应
    event Sent(address from, address to, uint amount);

    // 构造函数代码仅在合约创建时运行
    constructor() {
        minter = msg.sender;
    }

    // 向指定地址铸造一定数量的新币
    // 仅合约创建者可以调用
    // 相当于只有合约创建者可以向别人发送新币
    function mint(address receiver, uint amount) public {
        require(msg.sender == minter);
        balances[receiver] += amount;
    }

    // 错误(Errors)允许提供有关操作失败原因的信息
    // 这些信息会返回给调用该函数的用户
    error InsufficientBalance(uint requested, uint available);

    // 发送一定数量的现有币
    // 任何人都可以调用,将代币发送至指定地址
    function send(address receiver, uint amount) public {
    	// 发送的数量必须小于等于自己拥有的数量
        require(amount <= balances[msg.sender], InsufficientBalance(amount, balances[msg.sender]));
        // 发送者减少
        balances[msg.sender] -= amount;
        // 接收者增加
        balances[receiver] += amount;
        emit Sent(msg.sender, receiver, amount);
    }
}

代码 address public minter; 声明了一个 address 类型的状态变量。

address 类型是一个 160 位的值,不允许执行任何算术运算。它适用于存储合约地址,或者存储外部账户(EOA)公钥哈希的一部分。

关键字 public 会自动生成一个函数,使外部可以访问当前合约的状态变量。如果没有 public,其他合约将无法访问该变量。

编译器生成的代码等效于以下函数(暂时忽略 external 和 view 关键字):

function minter() external view returns (address) { 
    return minter; 
}

下一行代码:

mapping(address => uint) public balances;

这行代码同样定义了一个 public 状态变量,但它的类型比 address 更复杂。mapping 是 Solidity 提供的一种映射类型,它将地址映射到 uint(无符号整数),即每个地址对应一个余额。

mapping 的特性:

  • mapping 类似于哈希表,所有可能的键在初始化时就已经存在,并默认映射到 0(即字节表示全为零)。

  • 无法获取 mapping 的所有键或所有值,因此如果你需要跟踪存储在 mapping 中的数据,最好自己维护一个列表,或者使用更合适的数据结构。

使用 mapping 是因为它提供了一种高效且简洁的方式来关联每个地址与其余额,且适应了区块链中分布式账本的特点。

由于 balances 变量是 public,编译器会自动生成以下 getter 函数:

function balances(address account) external view returns (uint) {
    return balances[account];
}

这个函数可以用于查询某个账户的余额,例如:

uint myBalance = contract.balances(myAddress);

这样,你就可以直接在外部访问某个地址的 balance,而无需手动编写 getter 方法。

这一行代码:

event Sent(address from, address to, uint amount);

声明了一个 事件(event),它在 send 函数的最后一行被触发(emit)。像 Web 应用程序这样的以太坊客户端可以监听这些事件,而不会产生太多成本。

当事件被触发后,监听器会立即收到 from、to 和 amount 这三个参数,从而能够跟踪交易。

刚才提到的以太坊客户端使用以下 JavaScript 代码(web3.js)监听 Sent 事件,并调用 balances 函数来更新用户界面:

Coin.Sent().watch({}, '', function(error, result) {
    if (!error) {
        console.log("Coin transfer: " + result.args.amount +
            " coins were sent from " + result.args.from +
            " to " + result.args.to + ".");
        console.log("Balances now:\n" +
            "Sender: " + Coin.balances.call(result.args.from) +
            "Receiver: " + Coin.balances.call(result.args.to));
    }
});

构造函数是一种特殊的函数,在合约创建时执行,且无法在之后被调用。

在这个合约中,构造函数会永久存储创建合约的人的地址:

constructor() {
    minter = msg.sender;
}

其中,msg 是 Solidity 提供的全局变量,它包含了一些区块链相关的属性,比如msg.sender为当前调用该函数的外部账户(EOA)或合约地址。

这个合约有两个主要的用户调用函数:

  • mint —— 铸造新币

  • send —— 发送已存在的币

mint(铸造新币)

function mint(address receiver, uint amount) public {
    require(msg.sender == minter);
    balances[receiver] += amount;
}

只有合约的创建者(minter)可以调用 mint,因为:

require(msg.sender == minter);

如果 msg.sender 不是 minter,则交易会被回滚(revert)。

balances[receiver] += amount; 为接收者账户增加一定数量的新币。

注意: 虽然 minter 可以无限制铸造代币,但如果 balances[receiver] + amount 超过 uint 类型的最大值 2的256次方 - 1,就会导致溢出(overflow)。然而,Solidity 默认启用了 Checked arithmetic(溢出检查),所以如果溢出发生,交易会自动回滚。

send(发送币)

function send(address receiver, uint amount) public {
    require(amount <= balances[msg.sender], InsufficientBalance(amount, balances[msg.sender]));
    balances[msg.sender] -= amount;
    balances[receiver] += amount;
    emit Sent(msg.sender, receiver, amount);
}

任何人(已经拥有币的人)都可以调用 send,将币发送给其他人。

如果 msg.sender 的余额不足:

require(amount <= balances[msg.sender], InsufficientBalance(amount, balances[msg.sender]));

交易会回滚(revert),并返回 InsufficientBalance 错误,错误信息会提供给调用者,以便前端应用或区块浏览器能够显示失败的具体原因。

Solidity 允许在交易失败时提供更多的错误信息,以便前端应用可以更容易地调试或做出反应。

错误信息通过 revert 语句触发:

error InsufficientBalance(uint requested, uint available);

当 require 失败时,它会返回 InsufficientBalance,并提供请求的金额 requested 和可用余额 available。

注意,在这个例子中,所有的代币操作(如铸造、转账)都在合约内部完成,余额和交易信息是局部的,仅存储在合约的 balances 映射中。

普通区块链浏览器(如 Etherscan)只能显示以太坊全局账户余额,你不会在普通的区块浏览器中看到余额变化。

解决方案:监听 Sent 事件,并创建自己的区块链浏览器来跟踪交易记录和余额变化,但你查询合约地址(通过合约内部的查询函数),而不是代币持有人的地址。

区块链基础

区块链作为一个概念对于程序员来说并不难理解。大多数复杂性(如哈希、椭圆曲线加密、对等网络等)只是为了为平台提供一组特定的功能和承诺。一旦你接受了这些特性作为前提,你就不必担心底层技术——就像你不需要知道亚马逊的 AWS 是如何在内部工作的。

交易

区块链是一个全球共享的事务性数据库。这意味着每个人都可以通过参与网络来读取数据库中的条目。如果你想更改数据库中的内容,你必须创建一个所谓的“交易”,并且这个交易必须被所有其他参与者接受。

“交易”一词意味着你想要进行的更改(假设你同时想更改两个值)要么完全不做,要么完全应用。此外,在你的交易被应用到数据库时,其他交易不能修改它。

例如,假设有一个表格列出了所有账户的余额。如果请求从一个账户转账到另一个账户,数据库的事务性特征确保如果从一个账户扣除金额,这个金额始终会被加到另一个账户上。如果由于某种原因无法将金额添加到目标账户,源账户也不会被修改。

此外,交易总是由发送者(创建者)进行加密签名。这使得保护对数据库特定修改的访问变得简单。举个例子,只有持有账户密钥的人可以从中转移一定的货币。

区块

需要克服的一个主要问题是双重支付攻击:“如果在网络中有两个交易都想清空一个账户,该怎么办?”

解决方案是:只有其中一个交易可以是有效的,通常是先被接受的那个。

问题在于,“先”在对等网络中并不是一个客观的术语。

对此的抽象回答是:你不需要担心。一个全球公认的交易顺序会为你选定,从而解决冲突。这些交易会被打包成一个叫做“区块”的内容,然后被执行并在所有参与节点之间分发。如果两个交易互相矛盾,第二个交易会被拒绝,并不会成为区块的一部分。

这些区块形成了一个线性时间序列,这也是“区块链”这一术语的来源。区块会在定期的间隔时间内添加到链中,尽管这些间隔时间将来可能会发生变化。为了获取最新的信息,建议监控网络,例如通过 Etherscan。

可能会发生区块偶尔被回滚的情况,但仅限于“链顶”部分。这是因为越多的区块添加到某个区块上时,这个区块被回滚的可能性就越小。所以,可能会出现你的交易被回滚甚至从区块链中移除的情况,但等待的时间越长,这种情况发生的可能性就越小。

注意
交易并不能保证会包含在下一个区块或任何特定的未来区块中,因为是否将交易包含在区块中并不是由交易提交者决定的,而是由矿工决定交易被包含在哪个区块中。

如果我们想安排未来的智能合约调用,可以使用智能合约自动化工具(比如定时触发某个操作,或者基于某个事件触发合约的函数调用)或预言机服务。

预编译合约

在以太坊中,智能合约通常用 Solidity 编写,并转换为 EVM 字节码执行。但一些计算(例如椭圆曲线加密、哈希计算)如果用 Solidity 实现,会消耗大量 Gas,甚至无法在区块 Gas 限制内完成。因此,以太坊提供了一组内置的预编译合约。

地址范围 0x01 到 0x0a(包含 0x0a) 属于预编译合约(Precompiled Contracts)。这些合约可以像普通合约一样被调用,但它们的行为(包括 Gas 消耗)并不是由存储在这些地址上的 EVM 代码决定的。这些合约直接在 EVM 层面执行,比普通智能合约运行更高效,并且Gas 消耗更少。

在这里插入图片描述

这些合约特别适用于密码学、哈希计算、零知识证明等高计算量的任务。

http://www.dtcms.com/a/98140.html

相关文章:

  • Unity编辑器功能及拓展(1) —特殊的Editor文件夹
  • Linux 一键安装 Docker 的万能脚本
  • python和c中作用域的差异
  • Windows 系统中使用 fnm 安装 Node.js 的完整指南
  • 为什么idea显示数据库连接成功,但操作数据库时,两边数据不同步
  • Vite 开发服务器存在任意文件读取漏洞
  • Selenium文件上传
  • 使用 Avada 主题创建动态内容展示的技术指南
  • 尚硅谷面向对象篇笔记记录
  • 密文搜索 | 第六届蓝桥杯国赛C++B组
  • GMP调度模型
  • GAMMA数据处理(十)
  • RabbitMQ高级特性--发送方确认
  • AIOHTTP
  • 2025年3月电子学会c++五级真题
  • GOF23种设计模式
  • 树莓派5智能家居中控:HomeAssistant全配置指南
  • 笔记:基于环境语义的通感融合技术,将传统通信由“被动接收”转为“主动感知”
  • synchronized锁与lock锁的区别
  • 实变函数:集合与子集合一例(20250329)
  • JavaFX基础- Button 的基本使用
  • Linux进程管理之子进程的创建(fork函数)、子进程与线程的区别、fork函数的简单使用例子、子进程的典型应用场景
  • 【19期获取股票数据API接口】如何用Python、Java等五种主流语言实例演示获取股票行情api接口之沪深A股实时交易数据及接口API说明文档
  • 参加李继刚线下活动启发:未来提示词还会存在吗?
  • 【初阶数据结构】线性表之双链表
  • 【数电】半导体存储电路
  • 基于Linux平台安装部署Redis全教程
  • 生物化学笔记:医学免疫学原理09 白细胞分化抗原+黏附分子
  • Supplements of My Research Proposal: My Perspectives on the RAG
  • 数据结构:探秘AVL树