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.encode("aaa", "bbb"):
- 潜在风险:
- 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 函数在以下情况下被调用:
- 合约收到一个调用,但调用的函数签名(calldata)不匹配合约中的任何函数
- 如果没有定义 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 会按以下顺序决定调用哪个函数:
- 如果 calldata 匹配某个函数签名,调用该函数
- 如果 calldata 为空且发送了 ETH:
- 如果存在 receive 函数,调用 receive
- 如果没有 receive 函数,但有 payable fallback 函数,调用 fallback
- 如果两者都不存在,交易失败
- 如果 calldata 不为空且不匹配任何函数签名:
- 调用 fallback 函数(无论是否 payable)
- 如果没有 fallback 函数,交易失败
- 如果 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. 注意事项
- Gas 限制:
- transfer 和 send 方法只提供 2300 Gas,receive 或 fallback 函数的逻辑必须简单(例如仅记录事件或更新状态)
- 如果需要复杂逻辑,建议让用户通过 call 发送 ETH,提供更多 Gas
- 安全性:
- 避免在 receive 或 fallback 中调用外部合约(可能导致重入攻击)
- 使用 require 检查条件,确保合约安全
- Solidity 版本差异:
- 在 Solidity 0.6.0 之前,没有 receive 函数,payable fallback 处理所有 ETH 转账
- 在 0.6.0 及以上,推荐使用 receive 处理 ETH 转账,fallback 用于非标准调用
- 测试场景:
- 测试 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 的注意事项
- 合约接收 Ether 的逻辑:
- 使用 receive() 优先,简单且明确
- fallback() 适用于复杂逻辑,但需谨慎 Gas 消耗
- 如果目标地址是 EOA,无需额外逻辑
- Gas限制:
- 2300 Gas(由 transfer或 send)仅够简单操作(如余额更新或emit事件)
- 复杂逻辑(如存储更新)需通过 call 并提供更多Gas
- 安全实践:
- 检查-影响-交互模式(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 { // 安全转账逻辑 } }
- 检查-影响-交互模式(Checks-Effects-Interactions):先更新状态,再发送Ether,避免重入
六、代码示例
以下是一个综合示例,展示如何在智能合约中安全发送和接收 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
七、常见问题与注意事项
- 重入攻击:
- 攻击者可能通过 fallback 函数在接收 Ether 时重复调用发送者合约,导致资金被多次提取
- 解决:使用 ReentrancyGuard 或严格遵循检查-影响-交互模式
- Gas变化:
- 以太坊可能通过硬分叉调整 Gas 机制(如 EIP-150 调整了 2300 Gas 限制)
- 使用call更具未来兼容性
- 目标地址检查:
- 确保目标地址是 payable 类型
- 避免向零地址(address(0)) 发送 Ether
- 测试与审计:
- 使用工具(如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)的定义
- 事件(Events)
事件是 Solidity 提供的一种机制,用于在智能合约中定义和触发某些特定操作或状态变化的记录。事件本质上是一种声明,开发者通过 event 关键字定义事件,并在合约中通过 emit 关键字触发事件。触发的事件会生成日志,存储在区块链的交易收据(Transaction Receipt)中。 - 日志(Logs)
日志是事件触发后生成的数据记录,存储在以太坊区块链的交易收据中。日志是事件的具体输出,包含事件名称、参数值以及其他元数据(如合约地址、交易哈希等)。日志是区块链上不可篡改的记录,外部应用(如 DApp 或索引服务)可以通过监听日志获取合约状态变化。 - 事件与日志的关系
- 事件是开发者在合约中定义的模板,指定了要记录的数据结构。
- 日志是事件触发后在区块链上实际存储的数据。
- 事件触发时,EVM(以太坊虚拟机)会将事件数据编码为日志,记录在交易的日志部分。
二、事件的作用
事件在智能合约开发中有多种用途,主要包括以下几个方面:
- 状态变化通知
事件允许合约通知外部世界(如前端、后端、其他合约)发生了某些重要操作或状态变化。例如,代币转账时触发 Transfer 事件,通知用户代币已转移。 - 调试与监控
开发者可以通过事件记录合约的执行细节,便于调试和监控。例如,记录函数调用参数或中间计算结果。 - 数据索引与查询
事件日志可以被索引(通过 indexed 关键字),便于外部应用高效查询特定事件。例如,查询某个地址的所有代币转账记录。 - 节约 Gas 费用
相比于将数据直接存储在区块链的状态变量中,使用事件记录数据成本更低。事件数据存储在日志中,而不是状态存储中,因此 Gas 费用较低。 - 与外部系统集成
事件是 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 值
四、日志的存储与结构
日志是事件触发后生成的数据,存储在区块链的交易收据中。以下是日志的存储机制和结构:
- 存储位置
日志存储在交易的 logs 字段中,属于交易收据的一部分。交易收据可以通过交易哈希查询,包含以下信息:- 合约地址
- 日志数据(topics 和 data)
- 区块号
- 交易哈希
- Gas 使用情况等
- 日志结构
每条日志包含以下字段:- address:触发事件的合约地址
- topics:一个数组,包含事件签名和索引参数的哈希值
- Topics[0]:事件签名的哈希(keccak256(事件名(参数类型)))
- Topics[1..3]:indexed 参数的值(最多 3 个)
- data:非索引参数的编码数据,存储为字节数组
- blockNumber:日志所在区块的编号
- transactionHash:日志所属交易的哈希
- logIndex:日志在交易中的索引
- 存储成本
- 日志存储在区块链的交易收据中,而不是状态存储中,因此 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
七、注意事项与最佳实践
- 事件参数限制
- 最多 3 个 indexed 参数,超出会导致编译错误
- 非 indexed 参数存储在 data 中,查询成本较高,建议合理规划
- Gas 优化
- 使用事件比存储状态变量更节省 Gas,但仍需注意参数数量和类型。例如,uint256 比 string 更节省 Gas
- 避免在循环中频繁触发事件,可能导致 Gas 费用激增
- 事件不可逆
日志是区块链上的永久记录,无法修改或删除。因此,触发事件前需确保数据正确 - 事件不可直接在合约内访问
合约无法直接读取已触发的事件日志。需要通过外部工具(如 web3.js、ethers.js)查询 - 匿名事件
使用 anonymous 关键字定义事件,可以不生成事件签名,节省 Gas,但不推荐使用,因为会导致外部工具难以解析event Log(address indexed sender, uint256 value) anonymous;
- 事件与状态变量的权衡
- 如果数据需要频繁查询或在合约内使用,优先使用状态变量
- 如果数据仅用于通知或历史记录,优先使用事件
- 安全性
- 不要在事件中记录敏感信息(如私钥、未加密数据),因为日志是公开的
- 确保事件触发逻辑与状态变化一致,避免误导外部监听者
八、事件与日志的局限性
- 不可在合约内直接访问
日志数据仅存储在交易收据中,合约无法直接读取历史事件 - 查询效率
虽然 indexed 参数支持高效查询,但查询大量日志可能需要专门的索引服务 - 存储限制
日志数据会增加区块链节点的存储负担,需合理设计事件数量和参数 - 跨链限制
以太坊事件日志无法直接被其他区块链访问,跨链场景需要额外桥接机制
七 . 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 技术不断发展,保持好奇心和持续学习将帮助您在这个领域脱颖而出!