Solidity 大神之路之内功修炼第四章

发布于:2025-06-25 ⋅ 阅读:(18) ⋅ 点赞:(0)

Solidity 是以太坊智能合约的主要编程语言,集成了多个高级功能,使开发者能够编写健壮且可重用的代码。这次我们来深入探讨 Solidity 的核心概念,包括继承、哈希、ABI 编码(encoding)与解码(decoding)、fallback 函数、发送与接收 Ether、库(library)、事件(events)与日志(logs)以及时间逻辑。无论你是初学者还是有一定编程经验的开发者,这篇文章将为你提供清晰、实用的指导。

一.  Solidity 中的继承

继承是面向对象编程(OOP)的核心概念,在 Solidity 中同样至关重要。它允许一个合约(称为派生合约、子合约或子类)从另一个合约(称为父合约)继承数据和函数,从而实现代码重用。Solidity 支持多重继承,即一个子合约可以从多个父合约继承数据和方法。

1.  在 Solidity 中,继承通过 is 关键字实现。以下是一个示例:

contract A {
    string public constant A_NAME = "A";
    function getName() public virtual returns (string memory) {
        return A_NAME;
    }
}

contract B is A {
    string public constant B_NAME = "B";
    function getName() public pure override returns (string memory) {
        return B_NAME;
    }
}

在这个例子中,合约 B 继承自合约 A,可以访问 A 中的 A_NAME 和 getName() 函数。B 通过重写(overriding)getName() 函数返回自己的 B_NAME。注意,父合约中的函数需要标记为 virtual 以允许重写,而子合约中的函数需要标记为 override。

2.  重写与重载

  • 重写(Overriding):当子合约重新实现父合约中具有相同名称和签名的函数时,需要使用 virtual 和 override 关键字

  • 重载(Overloading):当函数名称相同但参数不同时,Solidity 允许定义多个同名函数,这不需要特殊关键字。例如:

function getName(string memory name) public pure returns (string memory) {
    return string(abi.encodePacked("My name is: ", name));
}

3.  继承链与构造函数

在继承链中,如果父合约有构造函数需要参数,子合约必须在实例化时传递这些参数。以下是两种传递方式:

contract Parent {
    uint public value;
    constructor(uint _value) {
        value = _value;
    }
}

contract Child is Parent {
    constructor(uint _value) Parent(_value) {
        // 直接在继承语句中传递
    }
}

继承链中的函数调用遵循“最后派生”原则,即调用最末端的子合约中定义的函数版本。

4.  应用场景

假设您正在开发一个去中心化投票系统,包含一个基础投票合约 VotingBase,提供投票功能。其他合约(如 Election)可以继承 VotingBase,添加特定功能(如候选人管理)。

1. 关键点
  • 使用 virtual 标记父合约中可被重写的函数,子合约使用 override 重写

  • 构造函数参数需通过继承语句或子合约构造函数传递

  • 继承链中,调用的是“最后派生”的函数实现

  • 状态变量不能与父合约同名同类型,否则会报错

2.  完整代码示例

以下是一个投票系统的继承示例,展示多重继承、重写和构造函数参数传递:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// 基础投票合约
contract VotingBase {
    uint256 public totalVotes;
    string public constant BASE_NAME = "VotingBase";

    constructor(uint256 _initialVotes) {
        totalVotes = _initialVotes;
    }

    function vote() public virtual {
        totalVotes++;
    }

    function getContractName() public virtual returns (string memory) {
        return BASE_NAME;
    }
}

// 候选人管理合约
contract CandidateManager {
    string[] public candidates;

    function addCandidate(string memory _name) public virtual {
        candidates.push(_name);
    }

    function getCandidateCount() public view returns (uint256) {
        return candidates.length;
    }
}

// 选举合约,继承 VotingBase 和 CandidateManager
contract Election is VotingBase, CandidateManager {
    string public constant ELECTION_NAME = "Election";
    mapping(address => bool) public hasVoted;

    constructor(uint256 _initialVotes) VotingBase(_initialVotes) {}

    // 重写 vote 函数,添加防重复投票逻辑
    function vote() public override {
        require(!hasVoted[msg.sender], "Already voted");
        hasVoted[msg.sender] = true;
        totalVotes++;
    }

    // 重写 getContractName 函数
    function getContractName() public override returns (string memory) {
        return ELECTION_NAME;
    }

    // 重写 addCandidate 函数,添加长度限制
    function addCandidate(string memory _name) public override {
        require(bytes(_name).length > 0, "Candidate name cannot be empty");
        candidates.push(_name);
    }

    // 重载 getContractName,接受参数
    function getContractName(string memory prefix) public pure returns (string memory) {
        return string(abi.encodePacked(prefix, ELECTION_NAME));
    }
}
代码说明
  • VotingBase 提供基础投票功能,CandidateManager 管理候选人列表

  • Election 继承两者,通过 override 重写 vote 和 addCandidate,添加防重复投票和候选人名称验证逻辑

  • getContractName 被重写和重载,展示不同签名的函数处理

  • 构造函数传递 _initialVotes 给 VotingBase

二 .  Hash、ABI 编码(encoding)与解码(decoding)

1. Hash

哈希是将任意数据转换为固定长度唯一整数的过程,广泛用于加密和数据完整性验证。Solidity 常用 keccak256 哈希算法,输出 256 位的哈希值。哈希具有以下特性:

  • 确定性:相同输入始终产生相同输出

  • 唯一性:不同输入产生相同输出的概率极低

  • 单向性:无法从输出逆推输入

  • 固定长度:无论输入大小,输出长度固定

在 Solidity 中,keccak256 是一个内置函数,接受字节数据(bytes 类型)作为输入,返回 32 字节的哈希值(bytes32 类型)

示例:比较两个字符串的哈希值:

function compareStrings(string memory str1, string memory str2) public pure returns (bool) {
    return keccak256(abi.encodePacked(str1)) == keccak256(abi.encodePacked(str2));
}

keccak256 的主要作用是将任意长度的数据输入映射为固定长度的 256 位哈希值,具有以下特性:

  • 确定性:相同的输入始终产生相同的哈希输出
  • 不可逆:无法从哈希值反推出原始输入
  • 抗碰撞:难以找到两个不同输入产生相同的哈希值
  • 均匀分布:输出哈希值在 256 位空间内均匀分布

在智能合约中,keccak256 常用于:

  • 数据完整性验证:确保数据未被篡改
  • 生成唯一标识:通过哈希生成唯一 ID 或键值
  • 签名验证:结合 ABI 编码生成消息哈希,用于验证签名(如 EIP-712)
  • 随机数生成:结合区块信息生成伪随机数
  • 数据压缩:将大数据(如字符串、结构体)压缩为固定长度的哈希,节省存储成本

keccak256 在以太坊智能合约中有广泛应用,包括但不限于:

  • 消息签名验证:在 EIP-712 或其他签名机制中,keccak256 用于生成消息的哈希,供用户签名和合约验证
    • 示例:验证用户签名的合法性,如在去中心化交易所(DEX)中授权交易
  • 唯一标识生成:通过对数据(如用户地址、时间戳)进行哈希,生成唯一标识
    • 示例:在 NFT 合约中生成代币 ID
  • 映射键:将复杂数据(如字符串、结构体)哈希为固定长度,用于映射(mapping)的键
    • 示例:通过哈希存储用户提交的表单数据
  • 链下计算验证:链下计算结果的哈希存储在链上,链上通过 keccak256 验证
    • 示例:零知识证明或 Merkle 树验证
  • 随机数生成:结合区块哈希、时间戳等生成伪随机数
    • 示例:彩票或游戏合约
  • 事件索引:在事件日志中,事件签名的哈希(由 keccak256 生成)用于索引
    • 示例:event Transfer(address indexed from, address indexed to, uint256 value) 的签名哈希

2. ABI 编码(encoding)与解码(decoding)

ABI(Application Binary Interface)编码是将数据转换为字节格式以便以太坊虚拟机(EVM)处理的过程,解码则是逆过程。abi.encode 将数据编码为字节,abi.decode 将字节转换回结构化数据。

1. ABI 编码解码的作用

ABI 编码和解码的主要作用是将 Solidity 的数据类型(如地址、整数、字符串等)转换为字节数组(bytes)或从字节数组还原为原始数据类型,以便:

  • 与智能合约交互:智能合约之间的调用或外部调用需要将参数编码为字节格式,符合以太坊 ABI 规范
  • 数据序列化:将复杂的数据结构(如结构体、数组)序列化为字节流,便于存储或传输
  • 跨链或外部系统交互:将数据编码为标准格式(如字节数组),以便与链下系统或其他区块链系统通信
  • 签名和验证:在加密签名(如 EIP-712)或消息验证中,数据需要编码为特定格式以生成哈希

解码则用于将编码后的字节数据还原为原始数据类型,以便在合约中进一步处理。

2. 使用场景

ABI 编码和解码广泛应用于以下场景:

  • 合约调用:在调用其他智能合约的函数时,参数需要通过 abi.encode 编码为字节数据,传递给 call 或 delegatecall
  • 事件日志:事件参数通常会被编码为字节格式存储在区块链日志中,解码用于解析日志数据
  • 低级调用:在低级操作(如 address.call())中,编码参数以构造调用数据
  • 消息签名:在 EIP-712 或其他签名机制中,数据需要编码为字节以生成哈希,供签名和验证
  • 存储优化:将数据编码为紧凑的字节数组,减少存储成本
  • 跨链桥:跨链协议中,数据需要编码为字节格式以便传输到其他链
3. 如何使用

Solidity 提供了内置的 abi.encode 和 abi.decode 函数,用于编码和解码数据。

编码:abi.encode
  • 功能:将参数编码为符合以太坊 ABI 规范的字节数组
  • 语法:abi.encode(arg1, arg2, ...),其中 arg1, arg2 是需要编码的参数
  • 返回值:bytes 类型的字节数组

示例

function encodeData(address _addr, uint256 _value) public pure returns (bytes memory) {
    return abi.encode(_addr, _value);
}
  • 上述代码将一个地址和一个整数编码为字节数组,输出类似: 0x000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000064 (其中 _addr 是 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48,_value 是 100)
解码:abi.decode
  • 功能:从字节数组中解码出原始数据类型
  • 语法:abi.decode(data, (type1, type2, ...)),其中 data 是字节数组,(type1, type2, ...) 是要解码的类型元组
  • 返回值:解码后的参数值

示例

function decodeData(bytes memory data) public pure returns (address, uint256) {
    return abi.decode(data, (address, uint256));
}
  • 如果传入上述编码的字节数组,解码后会返回地址 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 和整数 100
使用注意事项
  • 类型匹配:解码时指定的类型必须与编码时的类型一致,否则会抛出错误
  • 动态类型:动态类型(如字符串、数组)在编码时会包含长度信息,解码时需要正确处理
  • 低级调用示例
function callOtherContract(address _contract, address _addr, uint256 _value) public {
    bytes memory data = abi.encodeWithSignature("someFunction(address,uint256)", _addr, _value);
    (bool success, ) = _contract.call(data);
    require(success, "Call failed");
}
  • 这里 abi.encodeWithSignature 用于编码函数签名和参数,用于调用另一个合约的函数
4. abi.encode 和 abi.encodePacked 的区别
特性 abi.encode abi.encodePacked
编码方式 符合以太坊 ABI 规范,固定长度编码(每个参数填充到 32 字节)。 紧凑编码,不填充,尽量减少字节长度。
输出长度 固定为 32 字节的倍数,包含填充字节。 长度取决于实际数据,动态类型不填充。
使用场景 适用于合约调用、事件日志等需要 ABI 标准格式的场景。 适用于生成哈希、签名或节省存储空间的场景。
安全性 安全性高,数据格式明确,适合跨合约交互。 可能导致歧义(如短地址碰撞),需谨慎使用。
示例 abi.encode("aaa", "bbb") 输出固定长度字节。 abi.encodePacked("aaa", "bbb") 输出紧凑字节,可能导致 "aaabbb" 和 "aaa", "bbb" 编码相同。

详细对比

  • 编码结果
    • abi.encode("aaa", "bbb"):
      • 输出:0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000361616100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000036262620000000000000000000000000000000000000000000000000000000000
      • 每个字符串编码为 32 字节,包含长度和数据,填充到 32 字节边界。
    • abi.encodePacked("aaa", "bbb"):
      • 输出:0x616161626262
      • 直接拼接 "aaa" 和 "bbb" 的 ASCII 字节,无填充,长度为 6 字节。
  • 潜在风险
    • abi.encodePacked 可能导致数据歧义。例如,abi.encodePacked("aa", "ab") 和 abi.encodePacked("aaa", "b") 可能产生相同的字节输出,导致哈希碰撞
    • 因此,abi.encodePacked 在签名或哈希生成时需谨慎使用,建议搭配类型前缀或分隔符

使用建议

  • 使用 abi.encode 当需要与 ABI 兼容(如合约调用、事件日志)
  • 使用 abi.encodePacked 当需要紧凑的字节表示(如生成哈希、优化存储),但注意避免歧义
5. 代码示例:结合使用

以下是一个完整的 Solidity 示例,展示编码、解码和两者的区别:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ABITest {
    // 编码示例
    function encode(address _addr, uint256 _value) public pure returns (bytes memory) {
        return abi.encode(_addr, _value);
    }

    // 紧凑编码示例
    function encodePacked(address _addr, uint256 _value) public pure returns (bytes memory) {
        return abi.encodePacked(_addr, _value);
    }

    // 解码示例
    function decode(bytes memory data) public pure returns (address, uint256) {
        return abi.decode(data, (address, uint256));
    }

    // 调用其他合约
    function callContract(address _contract, address _addr, uint256 _value) public {
        bytes memory data = abi.encodeWithSignature("targetFunction(address,uint256)", _addr, _value);
        (bool success, ) = _contract.call(data);
        require(success, "Call failed");
    }

    // 生成哈希(使用 encodePacked)
    function hashData(string memory _str1, string memory _str2) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(_str1, _str2));
    }
}

以下是一个用户注册系统,展示哈希比较和 ABI 编码/解码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract UserRegistry {
    struct User {
        string username;
        address userAddress;
        uint256 registrationTime;
    }

    mapping(address => User) public users;

    // 比较用户名是否匹配
    function compareUsername(string memory _username1, string memory _username2) public pure returns (bool) {
        return keccak256(abi.encodePacked(_username1)) == keccak256(abi.encodePacked(_username2));
    }

    // 注册用户,编码用户信息
    function register(string memory _username) public returns (bytes memory) {
        require(bytes(_username).length > 0, "Username cannot be empty");
        users[msg.sender] = User(_username, msg.sender, block.timestamp);
        return abi.encode(_username, msg.sender, block.timestamp);
    }

    // 解码用户信息
    function decodeUser(bytes memory _data) public pure returns (string memory username, address userAddress, uint256 registrationTime) {
        (username, userAddress, registrationTime) = abi.decode(_data, (string, address, uint256));
    }

    // 验证用户数据
    function verifyUser(address _user, string memory _username) public view returns (bool) {
        return compareUsername(users[_user].username, _username);
    }
}

代码说明:

  • compareUsername 使用 keccak256 比较两个字符串的哈希

  • register 编码用户信息(用户名、地址、注册时间)并存储

  • decodeUser 解码字节数据,恢复用户结构体

  • verifyUser 验证用户输入的用户名与存储的是否一致

三 .  fallback 、receive 函数

1. fallback 函数

fallback 函数是一个特殊的函数,当智能合约接收到以太坊调用(例如通过 call 或直接发送数据)但调用无法匹配任何其他函数签名时,会触发该函数。它也可以处理合约接收 ETH 的情况(在某些条件下)。

定义
  • 语法
    fallback() external [payable] {
        // 函数体
    }
    • 必须声明为 external,因为它是由外部调用触发的
    • 可以选择是否为 payable,决定是否能接收 ETH
    • 不需要 function 关键字
触发条件

fallback 函数在以下情况下被调用:

  1. 合约收到一个调用,但调用的函数签名(calldata)不匹配合约中的任何函数
  2. 如果没有定义 receive 函数,且合约接收到 ETH 但没有附加数据(calldata 为空),payable fallback 函数会被调用
特点
  • Gas 限制:fallback 函数通常在低 Gas 环境中调用(例如通过 call),因此应尽量保持简单,避免复杂逻辑以防止 Gas 不足导致失败
  • payable 修饰符
    • 如果声明为 payable,可以接收  ETH
    • 如果没有 payable,则不能接收 ETH,发送 ETH 的交易会失败(除非有 receive 函数)
  • 用途
    • 处理未知函数调用,例如实现代理合约(Proxy Contract)转发调用
    • 记录或响应非标准调用
    • 在某些情况下处理 ETH 转账(当 receive 不存在时)
限制
  • 在 Solidity 0.6.0 及以上版本,fallback 函数不能作为接收 ETH 的默认函数,除非它是 payable 且 receive 函数不存在
  • 不建议在 fallback 中执行复杂逻辑,因为 Gas 限制可能导致失败

2. receive 函数

receive 函数是 Solidity 0.6.0 引入的,专门用于处理合约直接接收 ETH 的情况,且调用没有附加数据(calldata 为空)。

定义
  • 语法
    receive() external payable {
        // 函数体
    }
    • 必须声明为 external 和 payable
    • 没有参数和返回值
    • 不需要 function 关键字
触发条件

receive 函数在以下情况下被调用:

  • 合约接收到 ETH 转账,且 calldata 为空(即没有指定函数调用)
  • 如果 receive 函数不存在,且有 payable fallback 函数,则会调用 fallback 函数
  • 如果两者都不存在,直接向合约发送 ETH 的交易会失败
特点 
  • 专为 ETH 转账设计:receive 函数的唯一目的是处理没有附加数据的 ETH 转账
  • Gas 限制:与 fallback 类似,receive 函数通常在低 Gas 环境中调用(例如通过 transfer 或 send,提供 2300 Gas),因此逻辑应简单
  • 用途
    • 记录接收到的 ETH
    • 执行简单的逻辑,例如更新余额或触发事件
    • 适合需要处理 ETH 转账的场景,例如众筹合约
限制
  • 仅在 calldata 为空时触发。如果调用包含数据(即使是无效数据),则会尝试调用 fallback 函数
  • 必须声明为 payable,否则无法接收 ETH

3. fallback 和 receive 的区别

以下是 fallback 和 receive 的主要区别:

特性 receive fallback
定义方式 receive() external payable fallback() external [payable]
触发条件 接收 ETH 且 calldata 为空 未知函数调用,或接收 ETH(若无 receive)
是否需要 payable 必须是 payable 可选(若为 payable 可接收 ETH)
主要用途 处理纯 ETH 转账 处理未知函数调用或 ETH 转账(次要)
Solidity 版本 0.6.0 及以上 所有版本
调用优先级

当合约接收到一个调用时,Solidity 会按以下顺序决定调用哪个函数:

  1. 如果 calldata 匹配某个函数签名,调用该函数
  2. 如果 calldata 为空且发送了 ETH:
    • 如果存在 receive 函数,调用 receive
    • 如果没有 receive 函数,但有 payable fallback 函数,调用 fallback
    • 如果两者都不存在,交易失败
  3. 如果 calldata 不为空且不匹配任何函数签名:
    • 调用 fallback 函数(无论是否 payable)
    • 如果没有 fallback 函数,交易失败
  4. 如果 calldata 不为空且发送了 ETH,调用 fallback 函数(没有 payable),ETH发送失败

   

4. 用法和示例代码

以下是通过示例代码展示 fallback 和 receive 函数的用法。

示例 1:简单接收 ETH
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleWallet {
    event Received(address sender, uint256 amount);
    event FallbackCalled(address sender, uint256 amount, bytes data);

    // 记录接收到的以太币
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }

    // 处理未知函数调用
    fallback() external payable {
        emit FallbackCalled(msg.sender, msg.value, msg.data);
    }
}

解释

  • 当向合约发送 ETH(无 calldata),触发 receive 函数,记录发送者和金额
  • 当调用一个不存在的函数(例如通过 call 发送无效数据),触发 fallback 函数,记录调用信息
  • 如果 receive 不存在,且 fallback 是 payable,fallback 会处理 ETH 转账
示例 2:代理合约使用 fallback
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Proxy {
    address public implementation; // 目标合约地址

    constructor(address _implementation) {
        implementation = _implementation;
    }

    // 转发所有未知调用到目标合约
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
        require(success, "Delegatecall failed");
    }
}

解释

  • 这个代理合约通过 fallback 函数捕获所有调用,并使用 delegatecall 将调用转发到目标合约
  • payable 允许合约接收 ETH 并转发
  • 这种模式常用于可升级合约模式(Upgradeable Proxy Pattern)
示例 3:仅使用 receive 处理 ETH
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Crowdfunding {
    address public owner;
    uint256 public totalFunded;

    constructor() {
        owner = msg.sender;
    }

    // 仅处理以太币转账
    receive() external payable {
        totalFunded += msg.value;
        emit Funded(msg.sender, msg.value);
    }

    event Funded(address contributor, uint256 amount);
}

解释

  • 合约通过 receive 函数接收 ETH,并更新 totalFunded 状态变量
  • 适合众筹或简单的钱包合约场景
  • 如果调用包含 calldata,且没有 fallback 函数,交易会失败

5. 注意事项

  1. Gas 限制
    • transfer 和 send 方法只提供 2300 Gas,receive 或 fallback 函数的逻辑必须简单(例如仅记录事件或更新状态)
    • 如果需要复杂逻辑,建议让用户通过 call 发送 ETH,提供更多 Gas
  2. 安全性
    • 避免在 receive 或 fallback 中调用外部合约(可能导致重入攻击)
    • 使用 require 检查条件,确保合约安全
  3. Solidity 版本差异
    • 在 Solidity 0.6.0 之前,没有 receive 函数,payable fallback 处理所有 ETH 转账
    • 在 0.6.0 及以上,推荐使用 receive 处理 ETH 转账,fallback 用于非标准调用
  4. 测试场景
    • 测试 receive:通过 web3.eth.sendTransaction({to: contractAddress, value: amount}) 发送 ETH,无 data
    • 测试 fallback:调用不存在的函数,例如 contract.someInvalidFunction() 或发送带有无效 calldata 的交易

6. 实际应用场景

  • Crowdfunding(众筹):使用 receive 记录捐款
  • Proxy Contracts(代理合约):使用 fallback 转发调用到实现合约
  • Fallback Logging:记录所有未知调用以进行调试或监控
  • Simple Wallets:使用 receive 收集 ETH  并触发事件

以下是一个市场合约,展示 fallback  函数 和 receive 函数的用法:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Marketplace {
    uint256 public buyCount;
    uint256 public receiveCount;
    uint256 public fallbackCount;
    mapping(address => uint256) public userBuys;
    mapping(address => uint256) public userFallbacks;

    event BuyItem(address buyer, uint256 itemId, uint256 amount);
    event ReceivedETH(address sender, uint256 amount);
    event FallbackTriggered(address sender, uint256 amount, bytes data);

    // 模拟商品购买
    function buyItem(uint256 _itemId) public payable {
        require(msg.value > 0, "Payment required");
        buyCount++;
        userBuys[msg.sender]++;
        emit BuyItem(msg.sender, _itemId, msg.value);
    }

    // fallback 函数记录未匹配调用
    fallback() external payable {
        fallbackCount++;
        userFallbacks[msg.sender]++;
        emit FallbackTriggered(msg.sender, msg.value, msg.data);
    }

    // 接收 ETH
    receive() external payable {
        receiveCount++;
        emit ReceivedETH(msg.sender, msg.value);
    }
}

contract MarketplaceCaller {
    // 测试调用不存在的函数
    function callNonExistent(address payable _marketplace) public payable {
        (bool success, ) = _marketplace.call{value: msg.value}(
            abi.encodeWithSignature("nonExistentFunction()")
        );
        require(success, "Call failed");
    }
}

四 .  发送与接收 Ether

一、Ether发送与接收的基本概念

在以太坊中,Ether是原生加密货币,用于支付交易费用(Gas)、转账或与智能合约交互。发送Ether通常涉及从一个账户(外部拥有账户EOA或合约账户)向另一个账户转移资金,而接收Ether则是账户余额的增加。

1. 发送Ether的场景
  • 外部账户到外部账户(EOA to EOA):如用户A向用户B转账
  • 外部账户到合约账户(EOA to Contract):如用户向智能合约存款
  • 合约账户到外部账户(Contract to EOA):如智能合约向用户退款
  • 合约账户到合约账户(Contract to Contract):如一个合约调用另一个合约并附带Ether
2. 接收 Ether 的条件
  • 外部账户(EOA):无需特殊设置,接收 Ether 会自动增加账户余额

  • 合约账户:需要定义一个 receive 函数或 fallback 函数来处理接收的 Ether,否则交易会失败

    // 接收Ether的receive函数
    receive() external payable {}
    
    // 或者使用fallback函数
    fallback() external payable {}
    • receive():专门用于接收无数据(calldata 为空)的 Ether 转账,推荐使用
    • fallback():更通用,可处理带数据的调用或无receive函数时的Ether转账,但复杂度较高
3. Gas与安全性
  • 发送 Ether 的交易需要支付 Gas 费用,Gas Limit 决定了交易可消耗的计算资源
  • 智能合约发送Ether时需注意 重入攻击(Reentrancy Attack)Gas限制,稍后会详细说明

二、发送Ether的三种主要方式:transfer、send、call

在Solidity智能合约中,发送Ether主要通过以下三种方法实现:transfer、send和call。每种方法有不同的特性和适用场景。

1. transfer
  • 定义:address.transfer(uint256 amount) 是 Solidity 中用于发送 Ether 的内置方法,发送指定数量的 Ether 到目标地址
  • 特点
    • 固定Gas:transfer 转发 2300 Gas 给目标地址,用于简单的余额更新
    • 失败时抛出异常:如果转账失败(例如目标地址是合约但没有 receive 或 fallback 函数,或Gas不足),交易会自动回滚
    • 安全性较高:由于Gas限制,降低重入攻击风险
  • 用法
    contract TransferExample {
        function sendEther(address payable recipient, uint256 amount) external {
            recipient.transfer(amount); // 发送amount数量的Ether
        }
    }
  • 优点
    • 简单易用,适合大多数简单转账场景
    • 失败时自动回滚,减少错误处理代码
    • 2300 Gas 限制防止复杂逻辑执行,降低重入风险
  • 缺点
    • 2300 Gas 可能不足以执行复杂逻辑(如目标合约需要记录日志)
    • 不灵活,无法自定义 Gas 或处理失败情况
    • 如果以太坊未来调整 Gas 机制,可能会导致兼容性问题
  • 适用场景:向外部账户(EOA)或简单合约发送 Ether
2. send
  • 定义:address.send(uint256 amount) 是另一种发送 Ether 的内置方法,返回一个布尔值表示转账是否成功。
  • 特点
    • 固定Gas:与transfer相同,转发2300 Gas
    • 失败不抛异常:返回 false 而不是回滚,开发者需手动处理失败情况
    • 安全性较高:同样因 Gas 限制降低重入风险
  • 用法
    contract SendExample {
        function sendEther(address payable recipient, uint256 amount) external {
            bool success = recipient.send(amount); // 发送Ether
            require(success, "Transfer failed"); // 手动检查失败
        }
    }
  • 优点
    • 提供失败状态(布尔值),允许开发者自定义错误处理逻辑
    • Gas限制与transfer相同,安全性较高
  • 缺点
    • 需要手动检查返回值,增加代码复杂度
    • 2300 Gas限制可能导致失败(如目标合约逻辑复杂)
    • 较少使用,因为transfer更简单,call更灵活
  • 适用场景:需要自定义失败处理逻辑的场景,但实际中较少使用
3. call
  • 定义:address.call{value: amount}("") 是低级调用方法,可用于发送 Ether 并调用目标地址的函数
  • 特点
    • 灵活 Gas:开发者可通过 gas 参数自定义转发 Gas 量,默认转发所有可用 Gas
    • 返回值:返回(bool success, bytes memory data),表示调用是否成功及返回值
    • 不自动回滚:失败时返回 false,需手动处理
    • 功能强大:可调用目标合约的任意函数(若附带calldata)
  • 用法
    contract CallExample {
        function sendEther(address payable recipient, uint256 amount) external {
            (bool success, bytes memory data) = recipient.call{value: amount}("");
            require(success, "Transfer failed"); // 手动检查失败
        }
    }
    • 附带Gas限制的示例:
    (bool success, bytes memory data) = recipient.call{value: amount, gas: 5000}("");
  • 优点
    • 灵活性高,可自定义Gas,适应复杂目标合约
    • 可调用目标合约的函数(若提供 calldata)
    • 更符合以太坊未来发展方向,推荐使用
  • 缺点
    • 手动处理失败,容易遗漏错误检查
    • 默认转发所有 Gas 可能导致重入攻击,需谨慎
    • 代码复杂,需理解低级调用细节
  • 适用场景
    • 需要发送 Ether 到复杂合约(如有日志或其他逻辑)
    • 需要调用目标合约的特定函数
    • 未来向后兼容性要求高的场景

三、其他发送Ether的方式

除了 transfer、send 和 call,还有一些特殊方式或变体用于发送 Ether。

1. 在构造函数中发送 Ether(不推荐)
  • 在合约部署时,可以通过构造函数附带Ether发送到某个地址,但通常由外部账户调用时控制
    contract DeploySend {
        constructor(address payable recipient) payable {
            recipient.transfer(msg.value); // 部署时发送Ether
        }
    }
  • 注意:初始化场景,但不常用,因逻辑复杂且不灵活
2. 自毁(selfdestruct)
  • 定义:selfdestruct(address payable recipient)销毁合约并将其余额发送到指定地址
  • 特点
    • 将合约所有Ether强制发送到目标地址,即使目标无 receive 或 fallback函数
    • 销毁合约,释放存储空间(早期可退还 Gas)
  • 用法
    contract SelfDestruct {
        function destroy(address destroy(address payable recipient) external {
            selfdestruct(recipient);
        }
    }
  • 优点
    • 强制发送,忽略目标地址限制
    • 可清理合约
  • 缺点
    • 销毁不可逆,风险极高
    • 以太坊 EIP-4758(2023年提出)可能禁用 selfdestruct,未来不推荐
  • 适用场景:合约生命周期结束时清理(极少使用)
3. 预计算地址转账(CREATE2)
  • 使用 CREATE2 操作码部署合约时,可附带 Ether 到新合约地址,但需通过低级汇编实现
  • 用法:高级场景(如工厂合约),一般开发者无需直接使用
4. 直接调用(Coinbase Transaction)
  • 矿工可通过 coinbase 地址(block.coinbase)接收挖矿奖励,但这是协议层机制,开发者无法直接调用

5. 比较所有方式

方法 Gas限制 失败处理 安全性 灵活性 推荐场景
transfer 2300 抛异常 简单转账
send 2300 返回false 需要失败处理
call 可调 返回false 中(需防重入) 复杂合约交互
selfdestruct 低(销毁) 合约清理(不推荐)

五、接收 Ether 的注意事项

  1. 合约接收 Ether 的逻辑
    • 使用 receive() 优先,简单且明确
    • fallback() 适用于复杂逻辑,但需谨慎 Gas 消耗
    • 如果目标地址是 EOA,无需额外逻辑
  2. Gas限制
    • 2300 Gas(由 transfer或 send)仅够简单操作(如余额更新或emit事件)
    • 复杂逻辑(如存储更新)需通过 call 并提供更多Gas
  3. 安全实践
    • 检查-影响-交互模式(Checks-Effects-Interactions):先更新状态,再发送Ether,避免重入
      contract SafeSend {
          mapping(address => uint) balances;
          function withdraw(uint amount) external {
              uint balance = balances[msg.sender]; // 检查
              balances[msg.sender] = 0; // 影响
              (bool success, ) = msg.sender.call{value: amount}(""); // 交互
              require(success, "Transfer failed");
          }
      }
    • 使用 OpenZeppelin 的 ReentrancyGuard 防止重入:
        import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
        contract NonReentrant is ReentrancyGuard {
            function withdraw() external nonReentrant {
                // 安全转账逻辑
            }
        }

六、代码示例

以下是一个综合示例,展示如何在智能合约中安全发送和接收 Ether:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract EtherWallet {
    mapping(address => uint256) public balances;

    // 接收Ether
    receive() external payable {
        balances[msg.sender] += msg.value;
    }

    // 使用transfer发送Ether
    function sendViaTransfer(address payable recipient, uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        recipient.transfer(amount);
    }

    // 使用send发送Ether
    function sendViaSend(address payable recipient, uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        bool success = recipient.send(amount);
        require(success, "Send failed");
    }

    // 使用call发送Ether(推荐)
    function sendViaCall(address payable recipient, uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Call failed");
    }

    // 查询余额
    function getBalance() external view returns (uint) {
        return balances[msg.sender];
    }
}

说明

  • 用户可向合约存款(通过receive函数)
  • 支持三种发送方式,开发者可测试不同场景
  • 采用检查-影响-交互模式,降低重入风险

以下是一个众筹合约,展示 ETH 的发送和接收:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Crowdfunding {
    address public owner;
    uint256 public goal;
    uint256 public raisedAmount;
    mapping(address => uint256) public contributions;

    constructor(uint256 _goal) {
        owner = msg.sender;
        goal = _goal;
    }

    // 接收 ETH
    receive() external payable {
        require(raisedAmount < goal, "Goal already reached");
        contributions[msg.sender] += msg.value;
        raisedAmount += msg.value;
    }

    // 提取资金(仅限拥有者)
    function withdraw() public {
        require(msg.sender == owner, "Only owner can withdraw");
        require(raisedAmount >= goal, "Goal not reached");
        (bool success, ) = payable(owner).call{value: address(this).balance}("");
        require(success, "Transfer failed");
    }

    // 退款给贡献者
    function refund(address _contributor) public {
        require(msg.sender == owner, "Only owner can refund");
        uint256 amount = contributions[_contributor];
        require(amount > 0, "No contribution found");
        contributions[_contributor] = 0;
        raisedAmount -= amount;
        (bool success, ) = payable(_contributor).call{value: amount}("");
        require(success, "Refund failed");
    }
}

代码说明:

  • receive 接收 ETH 并记录贡献

  • withdraw 允许拥有者提取资金(目标达成后)

  • refund 允许拥有者退款给贡献者,使用 call 发送 ETH

七、常见问题与注意事项

  1. 重入攻击
    • 攻击者可能通过 fallback 函数在接收 Ether 时重复调用发送者合约,导致资金被多次提取
    • 解决:使用 ReentrancyGuard 或严格遵循检查-影响-交互模式
  2. Gas变化
    • 以太坊可能通过硬分叉调整 Gas 机制(如 EIP-150 调整了 2300 Gas 限制)
    • 使用call更具未来兼容性
  3. 目标地址检查
    • 确保目标地址是 payable 类型
    • 避免向零地址(address(0)) 发送 Ether
  4. 测试与审计
    • 使用工具(如Hardhat、Foundry)测试所有转账场景
    • 智能合约需经过专业审计,防止漏洞

五 .  Solidity 库(library)

Solidity 库是一组无状态、可重用的函数集合,旨在解决常见编程问题。库具有以下特点:

  • 无状态:不存储数据(除常量外)

  • 不可继承:不能继承其他合约或被继承

  • 纯函数:所有函数为 pure 或 view

  • 独立部署:可作为独立合约部署,节省 gas 成本

1.  示例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

library WeirdMath {
    function applyFactor(int self) public pure returns (int) {
        return self * 100;
    }
}

contract StrangeMath {
    using WeirdMath for int;
    function multiply(int num) public pure returns (int) {
        return num.applyFactor();
    }
}

库可以通过点号调用或 using 关键字绑定到数据类型上,增强代码可读性。

2.  应用场景

在金融合约中,库可用于标准化的数学运算(如利息计算),提高代码复用性和 gas 效率。

1.  完整代码示例

以下是一个金融计算库及其使用:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

library FinancialMath {
    uint256 private constant PRECISION = 1e18;

    function calculateInterest(uint256 principal, uint256 rate, uint256 time) public pure returns (uint256) {
        return (principal * rate * time) / PRECISION;
    }

    function addAmount(uint256 base, uint256 amount) public pure returns (uint256) {
        return base + amount;
    }
}

contract Loan {
    using FinancialMath for uint256;

    uint256 public totalLoan;
    uint256 public constant RATE = 5e16; // 5% 利率

    function issueLoan(uint256 _amount, uint256 _duration) public {
        uint256 interest = _amount.calculateInterest(RATE, _duration);
        totalLoan = _amount.addAmount(interest);
    }

    function getTotalLoan() public view returns (uint256) {
        return totalLoan;
    }
}
2.  代码说明
  • FinancialMath 提供利息计算和金额加法函数

  • Loan 使用 using 关键字将库函数绑定到 uint256,简化调用

  • issueLoan 计算贷款利息并更新总额

六 .  Solidity 中的事件(events)与日志(logs)

一、事件(Events)和日志(Logs)的定义

  1. 事件(Events)
    事件是 Solidity 提供的一种机制,用于在智能合约中定义和触发某些特定操作或状态变化的记录。事件本质上是一种声明,开发者通过 event 关键字定义事件,并在合约中通过 emit 关键字触发事件。触发的事件会生成日志,存储在区块链的交易收据(Transaction Receipt)中。
  2. 日志(Logs)
    日志是事件触发后生成的数据记录,存储在以太坊区块链的交易收据中。日志是事件的具体输出,包含事件名称、参数值以及其他元数据(如合约地址、交易哈希等)。日志是区块链上不可篡改的记录,外部应用(如 DApp 或索引服务)可以通过监听日志获取合约状态变化。
  3. 事件与日志的关系
    • 事件是开发者在合约中定义的模板,指定了要记录的数据结构。
    • 日志是事件触发后在区块链上实际存储的数据。
    • 事件触发时,EVM(以太坊虚拟机)会将事件数据编码为日志,记录在交易的日志部分。

二、事件的作用

事件在智能合约开发中有多种用途,主要包括以下几个方面:

  1. 状态变化通知
    事件允许合约通知外部世界(如前端、后端、其他合约)发生了某些重要操作或状态变化。例如,代币转账时触发 Transfer 事件,通知用户代币已转移。
  2. 调试与监控
    开发者可以通过事件记录合约的执行细节,便于调试和监控。例如,记录函数调用参数或中间计算结果。
  3. 数据索引与查询
    事件日志可以被索引(通过 indexed 关键字),便于外部应用高效查询特定事件。例如,查询某个地址的所有代币转账记录。
  4. 节约 Gas 费用
    相比于将数据直接存储在区块链的状态变量中,使用事件记录数据成本更低。事件数据存储在日志中,而不是状态存储中,因此 Gas 费用较低。
  5. 与外部系统集成
    事件是 DApp 与区块链交互的桥梁。前端可以通过 web3.js 或 ethers.js 监听事件,实时更新用户界面;后端可以通过事件日志构建索引(如 The Graph)。

三、事件的定义与触发

在 Solidity 中,事件的使用包括定义、触发和参数设置。以下是具体实现方式:

1. 事件定义

事件通过 event 关键字定义,类似于函数声明,但不需要函数体。事件可以包含参数,用于记录数据。

event Transfer(address indexed from, address indexed to, uint256 value);
  • 参数说明
    • address indexed from:转账发起地址,indexed 表示该参数可被索引,便于查询
    • address indexed to:转账接收地址,同样可被索引
    • uint256 value:转账金额,未加 indexed,不可直接索引
    • 最多可以有 3 个 indexed 参数,因为 EVM 日志限制了 topics 的数量(1 个用于事件签名,3 个用于索引参数)
2. 事件触发

使用 emit 关键字触发事件,并传入相应参数。

function transfer(address _to, uint256 _value) public {
    // 假设有余额检查和更新逻辑
    emit Transfer(msg.sender, _to, _value);
}
  • 触发时机:事件通常在状态变化后触发,例如转账、权限变更等。
  • 执行顺序:事件触发是同步的,日志会在交易执行完成后记录到交易收据中。
3. 带索引的参数(Indexed Parameters)
  • 使用 indexed 关键字的参数会存储在日志的 topics 字段中,便于高效查询。
  • 非 indexed 参数存储在日志的 data 字段中,查询成本较高。
  • 示例日志结构:
    • Topics[0]:事件签名(keccak256("Transfer(address,address,uint256)"))
    • Topics[1]:from 地址
    • Topics[2]:to 地址
    • Data:value 值

四、日志的存储与结构

日志是事件触发后生成的数据,存储在区块链的交易收据中。以下是日志的存储机制和结构:

  1. 存储位置
    日志存储在交易的 logs 字段中,属于交易收据的一部分。交易收据可以通过交易哈希查询,包含以下信息:
    • 合约地址
    • 日志数据(topics 和 data)
    • 区块号
    • 交易哈希
    • Gas 使用情况等
  2. 日志结构
    每条日志包含以下字段:
    • address:触发事件的合约地址
    • topics:一个数组,包含事件签名和索引参数的哈希值
      • Topics[0]:事件签名的哈希(keccak256(事件名(参数类型)))
      • Topics[1..3]:indexed 参数的值(最多 3 个)
    • data:非索引参数的编码数据,存储为字节数组
    • blockNumber:日志所在区块的编号
    • transactionHash:日志所属交易的哈希
    • logIndex:日志在交易中的索引
  3. 存储成本
    • 日志存储在区块链的交易收据中,而不是状态存储中,因此 Gas 成本远低于状态变量。
    • Gas 费用:
      • 每个 topic:375 Gas
      • 每个 data 字节:8 Gas
      • 基础日志费用:375 Gas

外部应用(如 DApp、索引服务)可以通过监听和查询事件获取合约状态变化。可以使用 web3.js 或 ethers.js 监听事件,对于大规模事件查询,推荐使用 The Graph 或自定义索引服务。这些服务会扫描区块链日志,构建可查询的数据库。

六、事件的使用场景与示例

以下是一个简单的 ERC20 代币合约,展示事件的使用:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleToken {
    mapping(address => uint256) public balances;
    string public name = "Simple Token";
    string public symbol = "STK";
    uint256 public totalSupply = 1000000 * 10**18; // 1M tokens, 18 decimals

    // 定义事件
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    constructor() {
        balances[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply); // 铸币事件
    }

    function transfer(address _to, uint256 _value) public returns (bool) {
        require(_to != address(0), "Invalid address");
        require(balances[msg.sender] >= _value, "Insufficient balance");

        balances[msg.sender] -= _value;
        balances[_to] += _value;

        emit Transfer(msg.sender, _to, _value); // 触发转账事件
        return true;
    }

    function approve(address _spender, uint256 _value) public returns (bool) {
        require(_spender != address(0), "Invalid address");

        emit Approval(msg.sender, _spender, _value); // 触发授权事件
        return true;
    }
}

事件使用说明

  • Transfer 事件记录代币转账操作,from 和 to 被索引,便于查询
  • Approval 事件记录授权操作,符合 ERC20 标准
  • 构造函数中的 Transfer 事件记录代币初始分配

在去中心化拍卖中,事件可记录出价和拍卖结束,供前端实时更新用户界面:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Auction {
    address public highestBidder;
    uint256 public highestBid;
    uint256 public endTime;
    bool public ended;

    event BidPlaced(address indexed bidder, uint256 amount);
    event AuctionEnded(address winner, uint256 amount);

    constructor(uint256 _biddingTime) {
        endTime = block.timestamp + _biddingTime;
    }

    function bid() public payable {
        require(block.timestamp < endTime, "Auction has ended");
        require(msg.value > highestBid, "Bid too low");
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit BidPlaced(msg.sender, msg.value);
    }

    function endAuction() public {
        require(block.timestamp >= endTime, "Auction not yet ended");
        require(!ended, "Auction already ended");
        ended = true;
        emit AuctionEnded(highestBidder, highestBid);
    }
}
代码说明
  • BidPlaced 记录每次出价,AuctionEnded 记录拍卖结果

  • indexed 参数 bidder 便于按出价者查询日志

  • 前端可通过 Ethers.js 监听事件,更新 UI

 

七、注意事项与最佳实践

  1. 事件参数限制
    • 最多 3 个 indexed 参数,超出会导致编译错误
    • 非 indexed 参数存储在 data 中,查询成本较高,建议合理规划
  2. Gas 优化
    • 使用事件比存储状态变量更节省 Gas,但仍需注意参数数量和类型。例如,uint256 比 string 更节省 Gas
    • 避免在循环中频繁触发事件,可能导致 Gas 费用激增
  3. 事件不可逆
    日志是区块链上的永久记录,无法修改或删除。因此,触发事件前需确保数据正确
  4. 事件不可直接在合约内访问
    合约无法直接读取已触发的事件日志。需要通过外部工具(如 web3.js、ethers.js)查询
  5. 匿名事件
    使用 anonymous 关键字定义事件,可以不生成事件签名,节省 Gas,但不推荐使用,因为会导致外部工具难以解析
    event Log(address indexed sender, uint256 value) anonymous;
  6. 事件与状态变量的权衡
    • 如果数据需要频繁查询或在合约内使用,优先使用状态变量
    • 如果数据仅用于通知或历史记录,优先使用事件
  7. 安全性
    • 不要在事件中记录敏感信息(如私钥、未加密数据),因为日志是公开的
    • 确保事件触发逻辑与状态变化一致,避免误导外部监听者

八、事件与日志的局限性

  1. 不可在合约内直接访问
    日志数据仅存储在交易收据中,合约无法直接读取历史事件
  2. 查询效率
    虽然 indexed 参数支持高效查询,但查询大量日志可能需要专门的索引服务
  3. 存储限制
    日志数据会增加区块链节点的存储负担,需合理设计事件数量和参数
  4. 跨链限制
    以太坊事件日志无法直接被其他区块链访问,跨链场景需要额外桥接机制

七 .  Solidity 中的时间逻辑

Solidity 使用 block.timestamp 表示当前区块的生成时间(Unix 纪元以来的毫秒数)。时间单位包括 seconds、minutes、hours、days 和 weeks。示例:

pragma solidity ^0.8.0;

contract TimestampExample {
    // 存储上次更新的时间戳
    uint256 public lastUpdate;

    // 事件:记录时间更新
    event TimeUpdated(uint256 newTimestamp, string unit);

    // 构造函数:初始化时设置当前时间
    constructor() {
        lastUpdate = block.timestamp;
        emit TimeUpdated(lastUpdate, "seconds");
    }

    // 获取当前时间戳(秒)
    function getCurrentTimestamp() public view returns (uint256) {
        return block.timestamp;
    }

    // 更新时间戳
    function updateTimestamp() public {
        lastUpdate = block.timestamp;
        emit TimeUpdated(lastUpdate, "seconds");
    }

    // 检查自上次更新是否经过指定时间
    function hasTimePassed(uint256 _seconds) public view returns (bool) {
        return block.timestamp >= lastUpdate + _seconds;
    }

    // 时间单位转换示例
    function timeUnitExamples()
        public
        view
        returns (
            uint256 secondsExample,
            uint256 minutesExample,
            uint256 hoursExample,
            uint256 daysExample,
            uint256 weeksExample
        )
    {
        // 当前时间戳
        uint256 currentTime = block.timestamp;

        // 演示时间单位
        secondsExample = currentTime + 30 seconds; // 加30秒
        minutesExample = currentTime + 5 minutes; // 加5分钟
        hoursExample = currentTime + 2 hours; // 加2小时
        daysExample = currentTime + 1 days; // 加1天
        weeksExample = currentTime + 1 weeks; // 加1周

        return (
            secondsExample,
            minutesExample,
            hoursExample,
            daysExample,
            weeksExample
        );
    }

    // 计算距离未来的剩余时间
    function timeUntil(
        uint256 futureTimestamp
    )
        public
        view
        returns (
            uint256 secondsLeft,
            uint256 minutesLeft,
            uint256 hoursLeft,
            uint256 daysLeft,
            uint256 weeksLeft
        )
    {
        require(futureTimestamp > block.timestamp, "Future timestamp required");

        uint256 timeDiff = futureTimestamp - block.timestamp;

        secondsLeft = timeDiff;
        minutesLeft = timeDiff / 1 minutes;
        hoursLeft = timeDiff / 1 hours;
        daysLeft = timeDiff / 1 days;
        weeksLeft = timeDiff / 1 weeks;

        return (secondsLeft, minutesLeft, hoursLeft, daysLeft, weeksLeft);
    }
}

应用场景

在时间锁合约中,资金在特定时间后才能解锁,适合信托或奖励机制:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract TimeLock {
    address public beneficiary;
    uint256 public releaseTime;
    uint256 public amount;

    constructor(address _beneficiary, uint256 _lockDuration) payable {
        beneficiary = _beneficiary;
        releaseTime = block.timestamp + _lockDuration;
        amount = msg.value;
    }

    function release() public {
        require(block.timestamp >= releaseTime, "Funds are still locked");
        require(address(this).balance >= amount, "Insufficient balance");
        (bool success, ) = payable(beneficiary).call{value: amount}("");
        require(success, "Transfer failed");
        amount = 0;
    }

    function getRemainingTime() public view returns (uint256) {
        if (block.timestamp >= releaseTime) return 0;
        return releaseTime - block.timestamp;
    }
}
代码说明
  • TimeLock 在指定时间后释放资金给受益人

  • release 检查时间戳,执行转账

  • getRemainingTime 返回剩余锁定期

代码仓库https://github.com/BraisedSix/Solidity-Learn 

总结:

通过掌握继承、哈希与 ABI 编码与解码、fallback 、receive 函数、ETH 发送与接收、库 library、事件 events 与日志 logs 和时间逻辑,您将能够编写更高效、安全的 Solidity 智能合约。建议编写代码实践这些示例,Web3 技术不断发展,保持好奇心和持续学习将帮助您在这个领域脱颖而出!


网站公告

今日签到

点亮在社区的每一天
去签到