DeFi 入门指南
从 Solidity 合约到 dApp 前端的完整开发教程
什么是 DeFi
DeFi(Decentralized Finance,去中心化金融)是构建在区块链上的金融协议生态。与传统金融不同,DeFi 不依赖银行、券商等中介机构,而是通过智能合约自动执行金融操作——交易、借贷、质押等。
在本教程中,我们将从零构建一个完整的 DeFi 小型项目:
SimpleToken (ERC20)
一个标准的 ERC20 代币合约,包含 name、symbol、decimals、totalSupply、transfer、approve、transferFrom 等完整功能。
SimpleDEX (AMM)
一个基于恒定乘积公式 x·y=k 的简单去中心化交易所,支持添加流动性和代币交换。
dApp 前端
使用 Next.js + ethers.js 构建的前端,连接 MetaMask 钱包,调用合约执行交易。
开发环境搭建
Foundry 是一个用 Rust 编写的高性能 Solidity 开发工具链。相比 Hardhat,Foundry 编译更快、测试用 Solidity 编写(而不是 JavaScript),且内置了丰富的链上交互工具。
安装 Foundry
curl -L https://foundry.paradigm.xyz | bash foundryup
初始化项目
forge init my-defi-project cd my-defi-project # 项目结构 / Project structure: # ├── src/ ← 合约代码 / Contract code # ├── test/ ← 测试文件 / Test files # ├── script/ ← 部署脚本 / Deploy scripts # └── foundry.toml ← 配置文件 / Config
forge
Solidity 编译器和测试框架,类似 Hardhat 但更快。用 Solidity 写测试而不是 JavaScript。
cast
命令行工具,可以直接与部署的合约交互、查询链上数据、编码/解码 ABI。
anvil
本地 Ethereum 节点,用于开发和测试。类似 Ganache 但原生集成。
chisel
交互式 Solidity REPL,可以快速实验 Solidity 代码片段。
编写 ERC20 代币合约
ERC20 是以太坊上最基础的代币标准。它定义了一组标准接口,让所有 ERC20 代币都可以被钱包、DEX 等通用工具识别和操作。下面是一个完整的实现:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleToken {
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
totalSupply = _initialSupply * 10 ** decimals;
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address to, uint256 amount) external returns (bool) {
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
require(balanceOf[from] >= amount, "Insufficient balance");
require(allowance[from][msg.sender] >= amount, "Insufficient allowance");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}approve + transferFrom 授权机制
ERC20 的核心设计模式是两步授权:用户先 approve 授权某个合约(如 DEX)可以使用自己的代币,然后 DEX 调用 transferFrom 把代币从用户账户转到自己账户。这种模式避免了直接向合约转账可能导致的安全问题。
编写简单 DEX 合约
我们的 SimpleDEX 使用恒定乘积公式 x·y=k 实现自动做市。这是 Uniswap V2 的核心算法——两种代币的储备量之积始终保持为常数 k。当有人买入 Token B 时,池中 Token A 增加、Token B 减少,价格自动调整。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./SimpleToken.sol";
contract SimpleDEX {
SimpleToken public tokenA;
SimpleToken public tokenB;
uint256 public reserveA;
uint256 public reserveB;
event LiquidityAdded(address indexed provider, uint256 amountA, uint256 amountB);
event Swapped(address indexed user, string direction, uint256 amountIn, uint256 amountOut);
constructor(address _tokenA, address _tokenB) {
tokenA = SimpleToken(_tokenA);
tokenB = SimpleToken(_tokenB);
}
function addLiquidity(uint256 amountA, uint256 amountB) external {
tokenA.transferFrom(msg.sender, address(this), amountA);
tokenB.transferFrom(msg.sender, address(this), amountB);
reserveA += amountA;
reserveB += amountB;
emit LiquidityAdded(msg.sender, amountA, amountB);
}
function swapAForB(uint256 amountA) external {
require(amountA > 0, "Amount must be > 0");
tokenA.transferFrom(msg.sender, address(this), amountA);
// 0.3% fee
uint256 fee = amountA * 3 / 1000;
uint256 amountInAfterFee = amountA - fee;
// Constant product: dy = y * dx / (x + dx)
uint256 amountOut = (reserveB * amountInAfterFee) / (reserveA + amountInAfterFee);
reserveA += amountA;
reserveB -= amountOut;
tokenB.transfer(msg.sender, amountOut);
emit Swapped(msg.sender, "A->B", amountA, amountOut);
}
function swapBForA(uint256 amountB) external {
require(amountB > 0, "Amount must be > 0");
tokenB.transferFrom(msg.sender, address(this), amountB);
uint256 fee = amountB * 3 / 1000;
uint256 amountInAfterFee = amountB - fee;
uint256 amountOut = (reserveA * amountInAfterFee) / (reserveB + amountInAfterFee);
reserveB += amountB;
reserveA -= amountOut;
tokenA.transfer(msg.sender, amountOut);
emit Swapped(msg.sender, "B->A", amountB, amountOut);
}
function getPrice() external view returns (uint256 priceAinB, uint256 priceBinA) {
require(reserveA > 0 && reserveB > 0, "No liquidity");
priceAinB = (reserveB * 1e18) / reserveA;
priceBinA = (reserveA * 1e18) / reserveB;
}
}Foundry 测试
Foundry 的测试用 Solidity 编写,使用 forge-std 库提供的 Test 基类。每个测试函数以 test 开头,使用 assertEq、assertTrue 等断言。vm.prank() 可以模拟不同地址调用。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/SimpleToken.sol";
import "../src/SimpleDEX.sol";
contract SimpleDEXTest is Test {
SimpleToken tokenA;
SimpleToken tokenB;
SimpleDEX dex;
address alice = address(0x1);
function setUp() public {
tokenA = new SimpleToken("Token A", "TKA", 1_000_000);
tokenB = new SimpleToken("Token B", "TKB", 1_000_000);
dex = new SimpleDEX(address(tokenA), address(tokenB));
// Transfer tokens to Alice for testing
tokenA.transfer(alice, 100_000 * 1e18);
tokenB.transfer(alice, 100_000 * 1e18);
}
function testAddLiquidity() public {
vm.startPrank(alice);
tokenA.approve(address(dex), 10_000 * 1e18);
tokenB.approve(address(dex), 10_000 * 1e18);
dex.addLiquidity(10_000 * 1e18, 10_000 * 1e18);
vm.stopPrank();
assertEq(dex.reserveA(), 10_000 * 1e18);
assertEq(dex.reserveB(), 10_000 * 1e18);
}
function testSwapAForB() public {
// Setup liquidity
vm.startPrank(alice);
tokenA.approve(address(dex), 10_000 * 1e18);
tokenB.approve(address(dex), 10_000 * 1e18);
dex.addLiquidity(10_000 * 1e18, 10_000 * 1e18);
// Perform swap
uint256 swapAmount = 1_000 * 1e18;
tokenA.approve(address(dex), swapAmount);
uint256 balanceBefore = tokenB.balanceOf(alice);
dex.swapAForB(swapAmount);
uint256 balanceAfter = tokenB.balanceOf(alice);
vm.stopPrank();
// Alice should have received some Token B
assertTrue(balanceAfter > balanceBefore);
// Reserve A should have increased
assertTrue(dex.reserveA() > 10_000 * 1e18);
// Reserve B should have decreased
assertTrue(dex.reserveB() < 10_000 * 1e18);
}
}运行测试
# 运行所有测试 / Run all tests forge test # 详细输出 / Verbose output forge test -vvvv # 运行特定测试 / Run specific test forge test --match-test testSwapAForB -vvvv
部署到测试网
Foundry 使用 Solidity 脚本进行部署。下面是部署到 Sepolia 测试网的完整脚本:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import "../src/SimpleToken.sol";
import "../src/SimpleDEX.sol";
contract DeployScript is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// Deploy tokens
SimpleToken tokenA = new SimpleToken("Alpha Token", "ALPHA", 1_000_000);
SimpleToken tokenB = new SimpleToken("Beta Token", "BETA", 1_000_000);
// Deploy DEX
SimpleDEX dex = new SimpleDEX(address(tokenA), address(tokenB));
// Add initial liquidity
tokenA.approve(address(dex), 50_000 * 1e18);
tokenB.approve(address(dex), 50_000 * 1e18);
dex.addLiquidity(50_000 * 1e18, 50_000 * 1e18);
vm.stopBroadcast();
console.log("Token A:", address(tokenA));
console.log("Token B:", address(tokenB));
console.log("DEX:", address(dex));
}
}部署命令
# 配置环境变量 / Set env vars
export PRIVATE_KEY=<your-private-key>
export SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/<api-key>
# 部署到 Sepolia / Deploy to Sepolia
forge script script/Deploy.s.sol:DeployScript \
--rpc-url $SEPOLIA_RPC_URL \
--broadcast \
--verify
# 导出 ABI / Export ABI
forge inspect SimpleToken abi > abi/SimpleToken.json
forge inspect SimpleDEX abi > abi/SimpleDEX.jsonABI 与合约交互
ABI(Application Binary Interface)是合约的接口描述,定义了合约有哪些函数、每个函数接受什么参数、返回什么类型。前端通过 ABI + 合约地址来调用链上合约。每个函数的前 4 字节 keccak256 哈希就是函数选择器。
ABI 浏览器
ABI JSON 结构
{
"type": "function",
"name": "transfer",
"stateMutability": "nonpayable",
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "bool"
}
]
}函数选择器
Foundry cast 命令
cast call <CONTRACT> "transfer(address,uint256)" <to> <amount>
参数说明
| 名称 | 类型 |
|---|---|
| to | address |
| amount | uint256 |
cast 命令行交互
# 查询余额 / Query balance
cast call <TOKEN_ADDRESS> "balanceOf(address)" <WALLET_ADDRESS> \
--rpc-url $SEPOLIA_RPC_URL
# 解码返回值 / Decode result
cast --to-dec <hex-result>
# 发送交易 / Send transaction
cast send <TOKEN_ADDRESS> "approve(address,uint256)" \
<DEX_ADDRESS> 1000000000000000000000 \
--private-key $PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URL
# 查看事件日志 / View event logs
cast logs --from-block latest "Transfer(address,address,uint256)" \
--address <TOKEN_ADDRESS> \
--rpc-url $SEPOLIA_RPC_URL前端项目搭建
现在我们用 Next.js + ethers.js v6 搭建前端。ethers.js 是最流行的以太坊 JavaScript 库,用于连接钱包、实例化合约、发送交易。
# 创建 Next.js 项目 / Create Next.js project npx create-next-app@latest my-defi-frontend cd my-defi-frontend # 安装 ethers.js v6 / Install ethers.js v6 npm install ethers
项目结构
my-defi-frontend/ ├── app/ │ ├── page.tsx # 主页面 │ └── layout.tsx ├── lib/ │ ├── contracts.ts # 合约地址和 ABI │ └── wallet.ts # 钱包连接逻辑 ├── abi/ │ ├── SimpleToken.json # 从 Foundry 导出 │ └── SimpleDEX.json └── package.json
合约配置
// lib/contracts.ts
import SimpleTokenABI from "../abi/SimpleToken.json";
import SimpleDEXABI from "../abi/SimpleDEX.json";
export const CONTRACTS = {
tokenA: {
address: "0x...", // 部署后填入
abi: SimpleTokenABI,
},
tokenB: {
address: "0x...",
abi: SimpleTokenABI,
},
dex: {
address: "0x...",
abi: SimpleDEXABI,
},
} as const;
export const SEPOLIA_CHAIN_ID = 11155111;连接钱包
ethers.js v6 中,BrowserProvider 替代了旧版的 Web3Provider。通过 window.ethereum(MetaMask 注入的对象)创建 provider,然后获取 signer 来签名交易。
// lib/wallet.ts
import { ethers } from "ethers";
import { SEPOLIA_CHAIN_ID } from "./contracts";
export async function connectWallet() {
if (!window.ethereum) {
throw new Error("MetaMask not installed");
}
// Request account access
await window.ethereum.request({
method: "eth_requestAccounts",
});
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
const network = await provider.getNetwork();
// Switch to Sepolia if needed
if (Number(network.chainId) !== SEPOLIA_CHAIN_ID) {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: ethers.toBeHex(SEPOLIA_CHAIN_ID) }],
});
}
return { provider, signer, address };
}
export function getContract(
address: string,
abi: ethers.InterfaceAbi,
signer: ethers.Signer
) {
return new ethers.Contract(address, abi, signer);
}在 React 组件中使用:
"use client";
import { useState } from "react";
import { connectWallet } from "@/lib/wallet";
export default function WalletButton() {
const [address, setAddress] = useState<string | null>(null);
async function handleConnect() {
try {
const { address } = await connectWallet();
setAddress(address);
} catch (err) {
console.error("Failed to connect:", err);
}
}
return (
<button onClick={handleConnect}>
{address
? `${address.slice(0, 6)}...${address.slice(-4)}`
: "Connect Wallet"}
</button>
);
}代币交换界面
代币交换需要两步操作:先 approve 授权 DEX 使用你的代币,然后调用 DEX 的 swap 函数。下面是完整的交互流程和模拟器。
Token 交换模拟器
流动性池状态
用户余额
前端 Swap 实现代码
// Swap component core logic
import { ethers } from "ethers";
import { CONTRACTS } from "@/lib/contracts";
import { getContract } from "@/lib/wallet";
async function handleSwap(signer: ethers.Signer, amount: string) {
const tokenA = getContract(
CONTRACTS.tokenA.address,
CONTRACTS.tokenA.abi,
signer
);
const dex = getContract(
CONTRACTS.dex.address,
CONTRACTS.dex.abi,
signer
);
const amountWei = ethers.parseUnits(amount, 18);
// Step 1: Approve
const approveTx = await tokenA.approve(
CONTRACTS.dex.address,
amountWei
);
await approveTx.wait();
console.log("Approved!");
// Step 2: Swap
const swapTx = await dex.swapAForB(amountWei);
const receipt = await swapTx.wait();
console.log("Swap done!", receipt.hash);
// Read updated balance
const balance = await tokenA.balanceOf(
await signer.getAddress()
);
console.log(
"New balance:",
ethers.formatUnits(balance, 18)
);
}Solidity ↔ ethers.js 对照
查询某地址的代币余额
// Solidity — ERC20
function balanceOf(address account)
external view returns (uint256)
{
return _balances[account];
}// ethers.js v6 const balance = await token.balanceOf( userAddress ); console.log( "Balance:", ethers.formatUnits(balance, 18) );
端到端完整流程
下面是我们构建的完整 dApp 架构图,展示了从用户交互到链上执行的完整数据流:
端到端流程总结
- 1. 用 Foundry 编写、测试、部署 SimpleToken 和 SimpleDEX 合约到 Sepolia
- 2. 从 Foundry 导出合约 ABI JSON 文件
- 3. Next.js 前端使用 ethers.js v6 连接 MetaMask 钱包
- 4. 通过 ABI + 合约地址创建合约实例
- 5. 用户点击 Swap → 前端构造 approve tx → MetaMask 签名 → 广播到链上
- 6. approve 确认后 → 构造 swap tx → 签名 → 广播 → 合约执行 → 返回结果
- 7. 前端监听事件、更新 UI 显示最新余额
总结与进阶
恭喜!你已经完成了一个完整的 DeFi 入门项目。从 Solidity 合约编写到 Foundry 测试部署,再到 ethers.js 前端集成,你已经掌握了 DeFi 开发的核心流程。
进阶方向
OpenZeppelin
使用 OpenZeppelin 标准库重写合约,学习 ERC721 (NFT)、ERC1155、Ownable、AccessControl 等模式。
多跳路由
实现多代币池和路由合约,支持 A→B→C 的多跳交换路径。
预言机集成
集成 Chainlink Price Feed 实现价格预言机,或实现 TWAP 时间加权平均价格。
前端进阶
添加交易历史、实时事件监听 (contract.on)、待处理交易状态管理。