简单理解
想象 RPC 节点就像银行的 ATM 机:
- ATM 机(RPC 节点)连接着银行的核心系统(区块链网络)
- 你通过 ATM 机(调用 RPC API)可以查询余额(读取链上数据)、转账(发送交易)
- 不同的 ATM 机可能由不同的银行或服务商提供(不同的 RPC 服务商)
约 14929 字大约 50 分钟
2025-06-12
本章将围绕 Web3 行业中的智能合约工程师所需的技能进行介绍,首先介绍去中心化应用(Dapp)的基本架构,重点分析其与传统应用的不同之处。接着,我们将详细讨论开发 Dapp 的流程,从需求分析、智能合约的编写、前端与后端的设计,到最终部署和上线,帮助读者理解整个开发生命周期。
去中心化应用(Dapp)是与传统集中式应用不同的全新应用模式,通常运行在区块链或分布式网络上。与传统应用相比,Dapp 的核心特点在于去中心化,意味着应用的逻辑和数据不由单一实体控制,而是由多个参与者共同维护。因此,开发 Dapp 需要理解和掌握去中心化技术栈、智能合约编程以及前端与区块链的交互方式。
Dapp 的架构主要由三个核心部分组成:
1. 前端(User Interface):
2. 智能合约(Smart Contracts):
3. 数据检索器(Indexer):
Event 形式释放日志事件,比如释放代表 NFT 转移的 Transfer 事件,数据检索器会检索这些数据并将其写入到 PostgreSQL 等传统数据库中Transfer 事件读取后写入传统数据库内,前端可以在传统数据库内检索用户持有的 NFT 数据4. 区块链和去中心化存储(Blockchain & Decentralized Storage):
Dapp 的开发流程可以分为以下几个阶段:

需求分析与规划
在开发 Dapp 之前,首先需要进行需求分析和规划,明确应用的目标和功能。此阶段包括:
智能合约开发
智能合约是 Dapp 的核心,负责执行去中心化的业务逻辑和存储重要的数据。在这一阶段,开发者需要:
检索器开发
检索器是获取链上数据的核心,负责捕获智能合约释放的事件并以合理的方式将其存入数据库的不同的表内部。在这一阶段,开发者需要:
前端开发
前端是用户与 Dapp 交互的主要界面,因此开发前端时需要:
与区块链交互
前端和智能合约通过 Viem(推荐)、Ethers.js 或 Wagmi 等现代化库进行交互。这些库提供更好的 TypeScript 支持和性能优化:
部署与上线
一旦开发完成,Dapp 进入部署阶段。具体步骤包括:
Dapp 的开发流程从需求分析、智能合约编写、前端开发,到最终部署,涵盖了多个技术栈的综合应用。开发 Dapp 时,区块链技术的透明性、不可篡改性以及智能合约的自动执行能力为应用提供了去中心化的基础。但与此同时,开发者需要应对与传统 Web 应用不同的挑战,如用户体验、交易处理以及安全性问题。
以太坊开发环境的搭建主要有以下几种常用方式,适合不同的开发需求:
开发环境安装命令(如未安装):
# 安装 nvm(如未装)。推荐参考文档 https://github.com/nvm-sh/nvm 安装最新版本
curl -o- <https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh> | bash
# 安装 Node.js LTS
nvm install --lts
nvm use --lts
# 安装 yarn(可选)
npm install -g yarn方式一:Foundry(Rust 实现,极快)
curl -L <https://foundry.paradigm.xyz> | bash
foundryupFoundry 提供以下以太坊开发工具:
forge: 帮助构建、测试、调试、部署和验证智能合约anvil: 本地开发节点,完全兼容以太坊 JSON-RPC 规范cast: 命令行工具,用于与链上应用交互初始化项目
forge init Counter测试合约
# Compile your contracts
forge build
# Run your test suite
forge test启动本地节点
anvil部署合约
# Use forge scripts to deploy contracts
# Set your private key
export PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
# Deploy to local anvil instance
forge script script/Counter.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --private-key $PRIVATE_KEY方式二:Hardhat(推荐,现代以太坊开发框架)
npm install --global hardhat
mkdir eth-dev && cd eth-dev
npx hardhat选择“创建一个基本示例项目”,会自动生成合约、测试和配置。
启动本地节点
npx hardhat node部署合约
npx hardhat run scripts/deploy.js --network localhostnpm install @openzeppelin/contracts在 Web3 开发中,RPC(Remote Procedure Call,远程过程调用) 是连接前端应用与区块链网络的关键桥梁。理解 RPC 的工作原理、选择合适的 RPC 服务商,以及正确配置和使用 RPC 节点,是每个 Web3 开发者必须掌握的基础知识。
RPC 是一种通信协议,允许应用程序通过网络调用远程服务器上的函数或方法。在区块链开发中,RPC 节点是运行区块链客户端软件的服务器,它们维护完整的区块链数据副本,并提供 API 接口供开发者查询链上数据、发送交易等操作。
简单理解
想象 RPC 节点就像银行的 ATM 机:
在 Dapp 开发中,RPC 节点承担着以下关键职责:
读取链上数据
发送交易
事件监听
网络管理
以太坊使用 JSON-RPC 2.0 协议作为标准的 RPC 通信格式。所有请求和响应都是 JSON 格式,通过 HTTP 或 WebSocket 传输。
基本请求格式:
{
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "latest"],
"id": 1
}基本响应格式:
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x1bc16d674ec80000"
}常用 JSON-RPC 方法:
| 方法名 | 功能 | 示例 |
|---|---|---|
eth_getBalance | 查询账户余额 | eth_getBalance(address, block) |
eth_blockNumber | 获取最新区块号 | eth_blockNumber() |
eth_sendTransaction | 发送交易 | eth_sendTransaction(txObject) |
eth_call | 调用合约(只读) | eth_call(callObject, block) |
eth_getTransactionReceipt | 获取交易收据 | eth_getTransactionReceipt(txHash) |
eth_getLogs | 查询事件日志 | eth_getLogs(filterObject) |
对于大多数开发者来说,自建节点成本高昂且维护复杂,因此通常会选择第三方 RPC 服务商。以下是主流服务商的对比:
| 服务商 | 特点 | 免费额度 | 适用场景 |
|---|---|---|---|
| Alchemy | 企业级服务,稳定性高,文档完善 | 每月 3 亿次请求 | 生产环境、企业应用 |
| Infura | 老牌服务商,ConsenSys 旗下 | 每月 10 万次请求 | 开发测试、中小型项目 |
| QuickNode | 高性能,低延迟,多链支持 | 有限免费额度 | 高频交易、实时应用 |
| Public Node | 完全免费,无需注册 | 无限制(可能有速率限制) | 学习测试、个人项目 |
| Ankr | 多链支持,去中心化节点网络 | 免费额度有限 | 多链应用开发 |
选择建议
以 Alchemy 为例,获取 RPC 端点的步骤:
注册账号
创建应用
获取 RPC URL
https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY使用 Viem(推荐):
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const client = createPublicClient({
chain: mainnet,
transport: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY')
})
// 查询余额
const balance = await client.getBalance({
address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'
})使用 Ethers.js:
const { ethers } = require('ethers')
const provider = new ethers.JsonRpcProvider(
'https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY'
)
// 查询余额
const balance = await provider.getBalance('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb')使用 Web3.js:
const { Web3 } = require('web3')
const web3 = new Web3('https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY')
// 查询余额
const balance = await web3.eth.getBalance('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb')在 hardhat.config.js 中配置:
require('@nomicfoundation/hardhat-toolbox')
module.exports = {
solidity: '0.8.19',
networks: {
mainnet: {
url: process.env.MAINNET_RPC_URL || '',
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
sepolia: {
url: process.env.SEPOLIA_RPC_URL || '',
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
}在 .env 文件中配置(不要提交到 Git):
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
PRIVATE_KEY=your_private_key_here保护 API Key
.gitignore 忽略 .env 文件错误处理和重试
429 速率限制、503 服务不可用)async function getBalanceWithRetry(address: string, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await client.getBalance({ address })
} catch (error) {
if (i === retries - 1) throw error
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
}
}
}速率限制管理
多节点备份
const rpcUrls = [
'https://eth-mainnet.g.alchemy.com/v2/KEY1',
'https://eth-mainnet.infura.io/v3/KEY2',
'https://rpc.ankr.com/eth'
]
async function callWithFallback(method: string, params: any[]) {
for (const url of rpcUrls) {
try {
const client = createPublicClient({
chain: mainnet,
transport: http(url)
})
return await client.request({ method, params })
} catch (error) {
console.warn(`RPC ${url} failed, trying next...`)
}
}
throw new Error('All RPC endpoints failed')
}监控和日志
本地节点开发
原因:请求频率超过了服务商的限制。
解决方案:
评估指标:
测试方法:
# 使用 curl 测试响应时间
time curl -X POST https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'自建节点:
第三方 RPC:
建议:
Solidity 是一种面向合约的高级编程语言,专门用于在以太坊虚拟机(EVM)上编写智能合约。它具有静态类型、支持继承、库和复杂的用户定义类型等特性。
版本声明
每个 Solidity 文件必须以版本声明开始:
pragma solidity ^0.8.0;数据类型
基本数据类型
| 类型 | 描述 | 示例 | 默认值 |
|---|---|---|---|
| bool | 布尔值 | true / false | false |
| uint8 | 8 位无符号整数 | 0 ~ 255 | 0 |
| uint16 | 16 位无符号整数 | 0 ~ 65535 | 0 |
| uint256 / uint | 256 位无符号整数 | 0 ~ (2^256 - 1) | 0 |
| int8 | 8 位有符号整数 | -128 - 127 | 0 |
| int256 / int | 256 位有符号整数 | -2^255 ~ (2^255 - 1) | 0 |
| address | 以太坊地址 | 0x…. | 0 |
| bytes1 ~ bytes32 | 固定长度字节数组 | bytes32 data = "Hello" | 0x00 |
| bytes | 动态字节数组 | bytes memory data = "Hello World" | "" |
| string | UTF-8 编码字符串 | string name = "Alice" | "" |
复合数据类型
| 类型 | 语法 | 描述 | 示例 |
|---|---|---|---|
| 静态数组 | T[k] | 固定长度数组 | uint[5] numbers |
| 动态数组 | T[] | 可变长度数组 | uint[] memory list |
| 映射 | mapping(K => V) | 键值对存储 | mapping(address => uint256) balances |
| 结构体 | struct | 自定义数据结构 | struct Person { string name; uint age; } |
| 枚举 | enum | 枚举类型 | enum Status { Pending, Active, Inactive } |
函数修饰符
可见性修饰符表
| 修饰符 | 可见范围 | 描述 | 使用场景 |
|---|---|---|---|
| public | 内部 + 外部 | 任何地方都可以调用 | 对外提供的公共接口 |
| external | 仅外部 | 只能从合约外部调用 | 外部用户接口,gas 效率更高 |
| internal | 内部 + 继承 | 当前合约和子合约可调用 | 内部逻辑函数,需要被继承 |
| private | 仅内部 | 只有当前合约可调用 | 私有实现细节 |
状态修饰符表
| 修饰符 | 状态读取 | 状态修改 | Gas 消耗 | 描述 |
|---|---|---|---|---|
| pure | ❌ | ❌ | 低 | 不读取也不修改状态的函数 |
| view | ✅ | ❌ | 低 | 只读取状态,不修改状态 |
| payable | ✅ | ✅ | 正常 | 可以接收以太币的函数 |
| 无修饰符 | ✅ | ✅ | 正常 | 可以读取和修改状态 |
开发范式
状态机模式
智能合约本质上是一个状态机,通过交易改变合约状态。
事件驱动编程
使用事件(Events)记录重要的状态变化,便于前端监听和日志记录。
模块化设计
通过继承和库(Library)实现代码复用和模块化。
基本结构
// 是 Solidity 中的单行注释符号,例如:// SPDX-License-Identifier: MIT 用于指定源代码的许可证类型。pragma 关键字用于声明 Solidity 源代码所需的编译器版本,确保合约在兼容的编译器环境中正确编译。contract 关键字用于定义一个智能合约,其语法格式为:contract 合约名 { ... }。// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyContract {
// 状态变量
uint256 public myNumber;
// 构造函数
constructor() {
myNumber = 100;
}
// 函数
function setNumber(uint256 _number) public {
myNumber = _number;
}
}状态变量(State Variables)
状态变量是指在合约中定义的、其值永久存储在区块链上的变量。它们用于记录合约的持久化数据,构成了合约的整体状态。当合约被部署后,这些变量将被写入区块链,并在合约的整个生命周期中保持可访问性和可追踪性。
contract MyContract {
/*
* 可以通过内部与外部函数更改变量
* public可以通过前端代码访问
*/
uint256 public totalSupply;
mapping(address => uint256) private balances;
address public owner;
// 常量
uint256 public constant MAX_SUPPLY = 1000000;
// 不可变量(构造函数中设置一次)
uint256 public immutable deploymentTime;
constructor() {
owner = msg.sender;
deploymentTime = block.timestamp;
totalSupply = 0;
}
}函数(Functions)
函数是 Solidity 智能合约中执行具体逻辑操作的核心组成部分。通过函数,可以实现对状态变量的读取、修改,或执行特定业务逻辑。
函数声明格式
Solidity 中函数的标准声明格式如下所示:
function <函数名>(<参数列表>)
<可见性>
<状态可变性>
<修饰符列表>
<虚拟/重写关键字>
returns (<返回值列表>)
{
// 函数体
}各部分含义如下:
<函数名>:函数的名称;<参数列表>:传入函数的参数;<可见性修饰符>:如 public、private、internal、external;<状态可变性修饰符>:如 view、pure、payable;<函数修饰符>:如 onlyOwner 等自定义逻辑控制;virtual/override:用于支持继承与函数重写;returns:定义返回值及其类型。函数可见性(Function Visibility)
函数可见性决定了函数在何种上下文中可以被调用
contract VisibilityExample {
// 仅当前合约可访问
function privateFunc() private pure returns(uint256) { return 1; }
// 当前合约和继承合约可访问
function internalFunc() internal pure returns(uint256) { return 2; }
// 所有人可访问
function publicFunc() public pure returns(uint256) { return 3; }
// 仅外部调用
function externalFunc() external pure returns(uint256) { return 4; }
}函数状态修饰符(State Mutability Modifiers)
用于指明函数是否修改或读取合约状态:
contract StateModifiers {
uint256 public count = 0;
// view: 只读函数,不修改状态
function getCount() public view returns(uint256) {
return count;
}
// pure: 纯函数,不读取也不修改状态
function add(uint256 a, uint256 b) public pure returns(uint256) {
return a + b;
}
// payable: 可接收以太币
function deposit() public payable {
// msg.value 是发送的以太币数量
}
// 默认:可修改状态
function increment() public {
count++;
}
}函数参数和返回值
Solidity 支持多参数与多返回值,以及命名返回值:
// 多个返回值
function getPersonInfo() public pure returns(string memory name, uint256 age) {
name = "Alice";
age = 25;
}
// 命名返回值
function calculate(uint256 a, uint256 b) public pure returns(uint256 sum, uint256 product) {
sum = a + b;
product = a * b;
// 自动返回命名变量
}
// 调用带多返回值的函数
function callExample() public pure {
(string memory name, uint256 age) = getPersonInfo();
// 或者忽略某些返回值
(, uint256 productOnly) = calculate(5, 3);
}修饰符(Function Modifiers)
修饰符允许在函数执行前插入额外逻辑,常用于权限控制与前置检查:
contract ModifierExample {
address public owner;
bool public paused = false;
constructor() {
owner = msg.sender;
}
// 自定义修饰符
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_; // 继续执行被修饰的函数
}
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
function togglePause() public onlyOwner {
paused = !paused;
}
// 使用多个修饰符
function criticalFunction() public onlyOwner whenNotPaused {
// 函数逻辑
}
}继承与函数重写(Inheritance and Override)
Solidity 支持单继承与多继承,子合约可重写父合约中的函数:
// 基础合约
contract Animal {
string public name;
constructor(string memory _name) {
name = _name;
}
function speak() public virtual returns(string memory) {
return "Some sound";
}
}
// 继承合约
contract Dog is Animal {
constructor(string memory _name) Animal(_name) {}
// 重写父类函数
function speak() public pure override returns(string memory) {
return "Woof!";
}
}
// 多重继承
contract Pet is Animal {
address public owner;
constructor(string memory _name, address _owner) Animal(_name) {
owner = _owner;
}
}
contract Labrador is Dog, Pet {
constructor(string memory _name, address _owner)
Dog(_name)
Pet(_name, _owner) {}
}接口与抽象合约(Interfaces & Abstract Contracts)
接口与抽象合约用于定义规范与继承框架:
// 接口定义
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
// 抽象合约
abstract contract AbstractToken {
string public name;
// 没有函数体的抽象函数,必须被子类使用 override 关键词重载实现
function totalSupply() public virtual returns (uint256);
// 有函数体实现的抽象函数,子类可以不使用 override 关键词重载直接继承已有的实现,也可以选择使用 override 关键词重载实现
function decimals() public view virtual returns (uint8) {
return 18;
}
}事件机制(Events)
事件用于在链上记录重要状态变化,并可由外部监听器(如检索器或前端应用)捕捉:
contract EventExample {
// 定义事件
event Transfer(address indexed from, address indexed to, uint256 amount);
event Approval(address indexed owner, address indexed spender, uint256 amount);
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
// 触发事件
// 可以在区块链浏览器查找到当前事件记录
emit Transfer(msg.sender, to, amount);
}
}常见攻击手段
| 风险点 | 攻击机理 | 典型防护措施 |
|---|---|---|
| Reentrancy | 恶意合约在 transfer / call 回调中再次进入受害函数,导致重复提款 | 1. Checks-Effects-Interactions 2. ReentrancyGuard(OpenZeppelin)3. 使用 transfer/send 或限制 gas(已不推荐,仅旧代码) |
| 访问控制 (Access Control) | 未受保护的管理函数可被任何人调用 | 1. Ownable:onlyOwner 修饰符 2. AccessControl:基于角色的权限(DEFAULT_ADMIN_ROLE, MINTER_ROLE 等)3. 及时转移 / 多签管理 |
| 整数溢出 (Integer Overflow / Underflow) | 旧版本 <0.8 加法/减法越界产生错误数值 | 1. Solidity 0.8+ 默认内置溢出检查 2. 对老版本使用 SafeMath 库 |
重入攻击(Reentrancy)防护
重入攻击(Reentrancy Attack)是智能合约中最常见且危害极大的安全漏洞之一。该攻击方式通常发生在合约向外部地址发送以太币或调用外部合约函数时,攻击者利用回调机制在合约状态更新之前重复调用受影响的函数,从而多次提取资金或重复执行某些操作,造成资产损失或逻辑混乱。
攻击原理简述
典型的重入攻击流程如下:
call 发送以太币);典型示例(易受攻击版本)
contract VulnerableContract {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// 危险:先转账,后更新状态
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] = 0; // 状态更新在转账之后
}
}防护措施
contract SecureContract {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// 先更新状态
balances[msg.sender] = 0;
// 后进行外部调用
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}contract ReentrancyGuard {
bool private locked;
modifier noReentrant() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
}
contract SecureWithGuard is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw() external noReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
balances[msg.sender] = 0;
(bool success,) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}访问控制(Access Control)
访问控制是保障智能合约安全性的核心机制之一。通过对关键函数设置访问权限,可以有效防止未经授权的用户执行敏感操作,从而避免资金被盗、状态被篡改等严重安全风险。
缺乏访问控制的风险示例
以下为一个存在严重安全漏洞的合约示例,任何地址均可提取合约内全部资金:
/**
* @title BadVault
* @dev 缺少访问控制,任何人都能调用 withdraw() 取走全部 ETH
*/
contract BadVault {
mapping(address => uint256) public balance;
// 用户存钱
function deposit() external payable {
balance[msg.sender] += msg.value;
}
// ❌ anyone can withdraw ALL funds!
function withdraw() public {
payable(msg.sender).transfer(address(this).balance);
}
}问题题说明: 该合约未对 withdraw 函数设置访问权限,攻击者可通过简单调用提取合约内所有以太币,造成资金全部流失。
安全的访问控制示例
通过引入显式权限判断,限制敏感操作的调用者,可以提升合约的安全性:
/**
* @title SafeVault
* @dev 仅部署者 (owner) 可以提取资金,简单显式访问控制
*/
contract SafeVault {
address public immutable owner; // 部署者地址
mapping(address => uint256) public balance;
// 构造函数:在部署时确定所有者地址
constructor(address owner_) {
owner = owner_;
}
// 存款函数:允许所有用户调用
function deposit() external payable {
balance[msg.sender] += msg.value;
}
// ✔️ 提款函数:仅限所有者调用
function withdraw() external {
// 进行访问权限判断
require(msg.sender == owner, "Not owner");
uint256 amount = address(this).balance;
require(amount > 0, "Nothing to withdraw");
// 注意 Checks-Effects-Interactions 顺序
(bool ok, ) = owner.call{value: amount}("");
require(ok, "Transfer failed");
}
}整数溢出防护(Integer Overflow Protection)
在早期版本的 Solidity(v0.8.0 之前),算术运算默认不进行溢出检查。这意味着当整数变量超出其最大值或最小值时,数值将环绕(wrap around),导致严重的逻辑漏洞和安全隐患。
受攻击的示例:无溢出检测的合约(< v0.8.0):
pragma solidity ^0.7.6; // ⚠️ 0.7 版本不会自动检查溢出
/**
* @title BadCounter
* @dev 用户每调用一次 `inc()`,计数器加 1;当计数器达到 10 停止奖励。
* 但整数溢出可让攻击者将计数器绕回 0,再无限领奖。
*/
contract BadCounter {
mapping(address => uint256) public counter;
mapping(address => bool) public rewarded;
// 计数 +1
function inc() external {
counter[msg.sender] += 1; // 若已 2**256-1 则回到 0
}
// 满 10 次领取 1 wei
function claim() external {
require(counter[msg.sender] >= 10, "not enough actions");
require(!rewarded[msg.sender], "already claimed");
rewarded[msg.sender] = true;
msg.sender.transfer(1); // 为演示简化为 1 wei
}
// 向合约注入少量 ETH 供演示
receive() external payable {}
}问题说明: 在该合约中,如果攻击者的 counter 达到最大值 2^256 - 1,再次调用 inc() 会使计数器绕回 0,从而绕过 >=10 的检查条件,并再次触发奖励逻辑,实现无限循环领取。
安全方案:限制上限 + 使用最新编译器版本
自 Solidity 0.8.0 起,所有算术运算默认开启溢出/下溢检查,若发生异常会自动 revert。除此之外,也建议通过逻辑限制控制最大值,防止边界绕回。
/**
* @title SafeCounter
* @dev 方案:在 inc() 中直接固定“最高 11”,超过即拒绝。
* 溢出永远不会发生,也杜绝了重复领奖。
*/
contract SafeCounter {
mapping(address => uint256) public counter;
uint8 constant MAX_ACTIONS = 11; // 上限 11,留 1 个缓冲
/// 受控递增:达到 10 后就不准再加
function inc() external {
require(counter[msg.sender] < MAX_ACTIONS, "limit reached");
counter[msg.sender] += 1;
}
/// 领取奖励
function claim() external {
require(counter[msg.sender] >= 10, "≥10 actions required");
counter[msg.sender] = 0; // 重置为 0
(bool ok, ) = msg.sender.call{value: 1}("");
require(ok, "transfer failed");
}
/// 注资
receive() external payable {}
}本章节将通过一个简单的"链上留言板"项目,介绍如何使用 Remix 开发、编译、部署并调用智能合约。
推荐使用 Remix IDE,它是一个基于浏览器的在线集成开发环境,提供完整的 Solidity 编写、编译、部署与调试功能,适合初学者快速上手。

本项目的智能合约实现了一个链上留言功能。每个用户地址可以在区块链上提交一条留言信息,所有留言将永久保存在链上,具有不可篡改与可溯源的特点。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MessageBoard {
// 保存所有人的留言记录
mapping(address => string[]) public messages;
// 留言事件,便于检索器和区块链浏览器追踪
event NewMessage(address indexed sender, string message);
// 构造函数,在部署时留言一条欢迎词
constructor() {
string memory initMsg = "Hello ETH Pandas";
messages[msg.sender].push(initMsg);
emit NewMessage(msg.sender, initMsg);
}
// 发送一条留言
function leaveMessage(string memory _msg) public {
messages[msg.sender].push(_msg); // 添加到发言记录
emit NewMessage(msg.sender, _msg); // 发出事件
}
// 查询某人第 n 条留言(从 0 开始)
function getMessage(address user, uint256 index) public view returns (string memory) {
return messages[user][index];
}
// 查询某人一共发了多少条
function getMessageCount(address user) public view returns (uint256) {
return messages[user].length;
}
}在 Remix IDE 左侧文件管理面板(File Explorer)中,点击"新建文件"按钮,创建一个名为 messageboard.sol 的 Solidity 合约文件,并将合约代码粘贴至该文件中。

如需重新组织项目结构,可点击 FILE EXPLORER 创建文件夹以便分类管理文件。




部署完成后,即可在 Remix 中调用合约的留言函数:
leaveMessage 函数输入框;Hello World);
| 对比维度 | 外部拥有账户 EOA | 合约账户 Contract Account |
|---|---|---|
| 地址来源 | keccak256(pubKey)[12:] (公钥 → 地址) | 创建时由 CREATE/CREATE2 计算 |
| 控制方式 | 私钥签名(用户、钱包) | 合约代码(EVM 字节码) |
| 状态字段 | nonce、balance | nonce、balance、codeHash、storageRoot |
| 能否发起交易 | ✅ 必须用私钥签名 | ❌ 只能由 EOA 触发或合约内部调用 |
| Gas 费用支付 | 由账户本身 ETH 余额承担 | 由调用者支付 |
| 典型场景 | 钱包地址、热冷账户 | ERC-20/721 Token、DeFi 协议、DAO |
| 术语 | 含义 | 备注 |
|---|---|---|
| Gas | 执行 1 条 EVM 指令的抽象工作量单位 | 汇编级别价格表见 evm.codes |
| Gas Limit (Tx) | 发送者愿为本笔交易消耗的 Gas 上限 | 防止死循环耗尽余额 |
| Gas Used | 实际执行指令花费的 Gas 总和 | 多退少不补 |
| Base Fee | 随区块动态调整的基础费用(EIP-1559) | 全网销毁,抑制拍卖狂飙 |
| Priority Fee / Tip | 发送者给出以激励打包者的附加费 | 给矿工 / 验证者 |
| Max Fee Per Gas | maxFee = baseFee + priorityFee 上限 | 钱包通常自动估算 |
nonce, to, value, data, gasLimit, maxFeePerGas, priorityFeePerGas, chainIdv, r, s 签名 → 序列化 RLPmaxFeePerGas、gasLimit、nonce 做基本筛查status, gasUsed, logsBloom, logs[])n ≥ 12 作"概率足够低"确认在第四章中,我们已经体验了使用 Remix 在本地虚拟机环境下部署合约的基本流程。然而,该过程仅为本地模拟,并未真正将合约发布到区块链网络。
接下来,我们将通过 测试网络(Testnet) 实现合约的正式部署,使其真正上链。
测试链部署的意义
测试网络是与主网结构一致的区块链网络,由真实的去中心化节点共同维护。与主网不同的是,测试网中的代币不具有实际经济价值,适用于:
一旦在测试链中部署成功且运行稳定,才建议将合约迁移至以太坊主网,以降低研发与运维成本。
浏览器可见性
由于测试链同样由真实节点组成,部署在测试链上的合约也可以通过以太坊区块浏览器(如 Sepolia Etherscan)进行查询与验证。
您可以在浏览器中查看:
以太坊测试网(Ethereum Testnets)是用于开发、测试和部署智能合约的网络环境,它们模拟主网功能但使用无价值的测试代币,让开发者可以安全地进行实验而无需承担真实的经济成本。
| 名称 | 共识机制 | 状态 | 主要特点 | 适用场景 |
|---|---|---|---|---|
| Sepolia | PoS (权益证明) | 活跃 | 长期支持的主要测试网,与主网最相似,稳定性高。 | 最终部署前测试,生产环境模拟,Dapp 集成测试 |
| Holesky | PoS (权益证明) | 活跃 | 专为验证者测试设计,大型网络规模,质押功能完整。 | 验证者节点测试, 质押协议开发,大规模网络测试。 |
在将合约部署至 Sepolia 测试网之前,需确保部署地址拥有足够的测试用 ETH(Sepolia ETH),以支付部署与调用智能合约所需的 Gas 费用。
获取 Sepolia 地址
0x 开头的以太坊地址,用于接收 Sepolia 测试币。

申请测试币
可以通过以下水龙头(Faucet)网址申请 Sepolia 测试代币:
操作步骤如下:
注意事项


在完成合约编写、编译和测试币领取等准备工作后,接下来可通过 Remix IDE 将合约正式部署到 Sepolia 测试网络,实现"上链"操作。
Remix 编译部署
连接钱包
打开 Remix IDE,点击右侧面板中的 Deploy & Run Transactions 模块,在 Environment 下拉菜单中选择:
Injected Provider - MetaMaskRemix 将自动调用浏览器中的 MetaMask 钱包,并连接到当前所选网络(确保已切换至 Sepolia 测试网)。

编译合约
切换至Solidity Compiler 面板,点击 Compile messageboard.sol 对合约进行编译,确保无错误提示。

部署合约
回到 Deploy & Run Transactions 面板:

查看部署结果
部署完成后,Remix 下方的命令行终端将输出相关日志,包括:

Etherscan 查看合约
部署成功后,我们可以借助 Etherscan (已默认选择测试网)区块浏览器 对部署过程及合约状态进行进一步验证与分析。
切换到 Sepolia 网络

通过交易哈希查看部署信息
部署合约时,MetaMask 会生成一条交易记录,其 Transaction Hash 可在 Remix 的命令终端或 MetaMask 历史记录中找到。将该哈希值粘贴到 Etherscan 的搜索框中,即可查看部署交易的详细信息,包括:

通过合约地址查看合约详情
0xfaC4dF6aA3b8265A96a7B269a55A88E2009F34Be);
查看合约事件日志(Events)
即使该合约尚未发生函数调用或转账交易,也可以通过 Etherscan 的 Events 标签页查看部署过程中由构造函数或初始设置触发的事件。该功能有助于调试初始化逻辑和验证合约状态。

合约交互
完成部署后,我们可以使用 Remix 提供的图形界面与已部署的智能合约进行函数调用测试,实现链上交互,并通过 Etherscan 验证交易与日志记录。
通过 Remix 调用合约函数
leaveMessage;Hello ETH);leaveMessage 按钮发起函数调用;
通过 Etherscan 验证交互结果



上述技术栈中,合约语言与一些前端 JS 区块交互的 API 最为基础,其他方面往往由其他厂商提供,如 RPC 节点、钱包等。
在第六章中,我们已成功将智能合约部署至测试网络。然而,仅部署合约并不足以实现用户交互。为此,本章节将通过构建一个基于 Web 的前端页面,使用户可以通过网页界面与区块链上的合约进行交互(如留言等操作),从而实现完整的链上功能闭环。

本前端界面基于以下技术栈构建:
Web3.js —— 用于与以太坊区块链进行交互注
完整源代码参考地址 👉: messageboard.html - GitHub
连接钱包
前端通过调用浏览器中的以太坊钱包插件(如 MetaMask)提供的 API,实现用户地址的连接与授权操作。
async function connectWallet() {
// 1. 请求用户授权账户访问
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts',
});
// 2. 创建 Web3 实例
web3 = new Web3(window.ethereum);
account = accounts[0];
// 3. 验证网络,本示例使用Sepolia 测试网
const chainId = await web3.eth.getChainId();
if (chainId !== 11155111) {
// Sepolia 测试网
// 网络错误处理
}
}合约 ABI 定义
const contractABI = [
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "message",
"type": "string"
}
],
"name": "NewMessage",
"type": "event"
},
......
];合约实例化
完成 ABI 导入与钱包连接后,通过 Web3.js 创建合约实例,绑定合约地址与 ABI,从而可直接调用合约中的函数接口。
function setContract() {
const address = document.getElementById('contractAddress').value.trim();
// 地址有效性验证
if (!web3.utils.isAddress(address)) {
// 错误处理
return;
}
// 创建合约实例
contract = new web3.eth.Contract(contractABI, address);
}合约方法调用
写入操作(需要 Gas)
核心概念:
.send() 方法用于执行状态改变的交易from 参数(发送者地址)transactionHash 等信息async function leaveMessage() {
const message = document.getElementById('messageInput').value.trim();
try {
// 调用合约的写入方法
const tx = await contract.methods.leaveMessage(message).send({
from: account,
});
// 获取交易哈希
console.log('交易哈希:', tx.transactionHash);
} catch (error) {
// 错误处理
}
}只读或模拟操作(免费)
.call() 方法用于执行只读查询或者模拟操作以判断某笔交易是否会成功async function queryMessages() {
const address = document.getElementById('queryAddress').value.trim();
try {
// 调用只读方法获取留言数量
const count = await contract.methods.getMessageCount(address).call();
// 批量获取留言内容
for (let i = 0; i < count; i++) {
const message = await contract.methods.getMessage(address, i).call();
// 处理消息内容
}
} catch (error) {
// 错误处理
}
}通过上述操作,前端即可实现用户连接钱包、链上留言、读取留言记录等功能,构建一个完整可用的 Dapp 原型。至此你已经了解合约代码,合约上链,前端交互整个大概流程。
基本原理与计量单位
减少存储操作(Storage Write)
读取存储第一次需 2100 gas(后续 100 gas),而内存读取仅 3 gas。推荐多次访问同一存储数据时,将其缓存到内存以减少 SLOAD 次数
每次写入 storage 的成本高达 20,000 gas;优先使用 memory。
示例:
// ❌ 非优化写法
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// ✅ 优化写法(一次读,一次写)
function deposit() public payable {
uint256 current = balances[msg.sender];
balances[msg.sender] = current + msg.value;
}使用位压缩(Bit Packing)
将多个变量压缩到一个 uint256 中以节省存储空间。
示例:
struct Packed {
uint128 a;
uint128 b;
}循环优化
减少不必要的运算,如 array.length 缓存到变量中。
示例:
// ❌ 非优化
for (uint256 i = 0; i < arr.length; i++) {
...
}
// ✅ 优化
uint256 len = arr.length;
for (uint i = 0; i < len; ++i) {
...
}函数可见性选择 - external 比 public 更节省 gas,适用于仅被外部调用的函数。
安全设计原则
重入攻击 Reentrancy
利用外部合约在 fallback 中重新调用原函数。历史上最著名的 The DAO 事件便因重入漏洞导致约 6000 万美元 ETH 被盗,最终造成以太坊社区分裂(形成 ETH/ETC 链)。
防护方法:先更新状态,再转账。
示例:
// ❌ 有漏洞
function withdraw() public {
require(balance[msg.sender] > 0);
(bool sent,) = msg.sender.call{value: balance[msg.sender]}("");
require(sent);
balance[msg.sender] = 0;
}
// ✅ 修复后
function withdraw() public {
uint256 amount = balance[msg.sender];
balance[msg.sender] = 0;
(bool sent,) = msg.sender.call{value: amount}("");
require(sent);
}预言机操纵 Oracle Manipulation
整数溢出/下溢
unchecked {} 时需确保逻辑安全。SafeMath。权限控制缺失
onlyOwner 或 AccessControl 修饰符保护。未初始化代理
前置交易/三明治攻击
审计必要性
slither MyContract.sol(也可指定合约地址)来扫描合约代码mythx analyze MyContract.sol 进行安全扫描forge test 运行所有测试forge test --match-path <test 文件路径> 定向运行特定测试文件。
| 机构 | 特点 | 项目经验 |
|---|---|---|
| 慢雾科技 | 国内领先,注重攻击复现 | EOS、币安、火币等 |
| OpenZeppelin | 社区信赖度高,基础库作者 | Compound、Balancer |
| ConsenSys Diligence | 精通以太坊底层原理 | Uniswap、1inch |

分支策略:
feature/功能描述 命名,完成后合并至 develop 分支。fix/bug-name 命名,优先合并到 develop 或 main。提交信息规范
使用简洁明确的提交信息,推荐格式:
类型: 简要描述
说明:可选的详细信息、动机、对比等
Issue:关联的 Issue 编号(如有)类型包括:
feat: 新功能fix: 修复问题docs: 文档更新refactor: 代码重构test: 添加或修改测试chore: 构建过程或辅助工具变动Pull Request(PR)流程
每个功能或修复应新建一个分支
提交 PR 前需确保:
本地通过所有测试
通过 ESLint 或其他静态检查工具
包含必要的文档或注释
PR 标题应简洁明了,描述改动内容
不允许合并自己创建的 PR,需至少一位 reviewer 审查通过
可在项目仓库中添加 PULL_REQUEST_TEMPLATE.md 文件,可统一 PR 描述格式,提升协作效率。例如:
## PR 说明
- 变更内容:简要描述此次 PR 完成了哪些功能或修复了哪些问题。
- 关联 Issue:填写相关 Issue 编号(如无可留空)。
- 主要改动:列出代码改动的关键点,如新增哪些函数、修改哪些逻辑等。
- 测试情况:说明已编写或执行了哪些测试来验证改动。
- 影响评估:列出可能影响的模块或兼容性问题(如版本依赖)。PR 标题应简洁表达功能,如 feat: 添加 staking 功能
Code Review 检查清单(代码评审时可按以下要点逐项检查):
- 代码是否符合风格规范,命名清晰、可读性高?
- 业务逻辑是否正确,边界情况和异常情况是否处理周全?
- 安全性检查:是否考虑重入、整数溢出、权限校验、外部调用等风险?
- Gas 消耗:是否避免了不必要的存储操作或大循环?
- 是否增加了足够的注释和文档说明?是否存在未使用的代码或死代码?
- 是否编写了充分的单元测试覆盖常见场景和极端情况?
描述 Issue 结构推荐:背景 + 问题 + 尝试过的方法 + 环境信息
示例:
### 问题描述
执行 `forge test` 时出现 `out of gas` 错误
### 环境
- Foundry 版本: 0.2.0
- Solidity: 0.8.21Issue 标签分类标准与自动化:
bug(缺陷)、enhancement(增强)、security(安全)、documentation(文档)、question(问题)等标签;high-priority、low-priority、help wanted 等。stale 等,减少人工维护成本。| 项目类型 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Optimistic Rollup | 设定欺诈证明期 | EVM 兼容性强,成本低 | 提现延迟(1~2 周) |
| ZK Rollup | 零知识证明 | 安全性高,支持快速提现 | 开发难度大,不完全 EVM 兼容 |
1. 环境准备
2. 合约部署注意事项
CREATE2 或存在 Gas 上限差异deploy() 命令或 hardhat starknet deploy3. 跨链交互与桥接
4. 案例推荐(可拓展)
03f55-add RPC knowledge section于 40f12-docs: clarify DApp frontend interaction with smart contracts via RPC于 6e5f9-Update smart-contract-development.md于 e33c8-update: 「智能合约开发」于 e1ddc-update: 「安全与合规」于 7648b-update: 更新「安全与合规」部分的常见网络安全风险与防护措施于 6607d-Fix smart contract content于 1a74b-Update images于 6cd86-Fix quotation marks于 584ed-Making a better URL于 1f8a3-Fix type consistency by changing "DApp" to "Dapp" across multiple documents in the Web3 handbook.于 23976-Add new images for Discord and Twitter setup, replace deleted DApp architecture diagram, and update related documentation in the Web3 handbook.于 91839-Update README.md and notes.ts to reflect the new smart contract development section, including updated links and improved clarity in the Web3 handbook.于 d4279-Update README.md to remove outdated mission and vision statements, and replace image references with newly added diagrams for better clarity in the Web3 handbook.于 f3309-Add contributing guidelines and enhance README structure for the Web3 internship handbook于 dcb45-Improve image name, English version tips, and outdated info于 ed32b-update: jason 的社交媒体信息于 a9dfe-update:隐藏「扩展阅读」于 5ad05-update:「合规和网络安全」补充扩展阅读、「智能合约」修改内容于 e0c39-update:part2 智能合约内容于 4d9e6-update: 检查所有 md 文件的中文文案排版于 75893-docs: 更新手册所有页面的扩展阅读、文章贡献者部分,以及工作习惯部分内容于 614c2-feat(docs): 优化首页内容与项目结构于 ea7fb-Init于 1c641-update: 所有页面底部新增文章贡献者于 22bd3-统一 Dapp 关键词于 be47d-update part2 page2/3 、part5 page1/2/3于 21196-更新 part1于 d134c-更新 part2于 0e360-update style于 版权归属:ETHPanda & LXDAO Community