Commit f8f2596d authored by Developer's avatar Developer

add fundry test

parent b20a2b0d
Pipeline #922 canceled with stages
# Foundry环境变量配置
# 部署者私钥(用于本地测试,请勿在生产环境使用)
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
# RPC URL (默认本地,可修改为远程RPC)
RPC_URL=http://127.0.0.1:8545
.env
out/
cache/
broadcast/
node_modules/
.DS_Store
*.swp
*.swo
*~
.idea
# 快速上手指南
## 🚀 5分钟快速开始
### 第一步:安装 Foundry(如果未安装)
```bash
# 安装 Foundry
curl -L https://foundry.paradigm.xyz | bash
# 重启终端后运行
foundryup
# 验证安装
forge --version
```
### 第二步:安装项目依赖
```bash
cd /Users/luxq/work/wuban/foundrytest
forge install foundry-rs/forge-std --no-commit
```
### 第三步:运行测试
```bash
# 方法1:使用一键测试脚本(推荐)
./test-all.sh
# 方法2:手动运行
forge test -vv
```
### 第四步:本地部署测试(可选)
```bash
# 终端1:启动本地测试网络
anvil
# 终端2:部署合约
./deploy-local.sh
```
## 📝 测试命令速查
```bash
# 编译合约
forge build
# 运行所有测试
forge test
# 运行测试并显示详细信息
forge test -vv
# 运行特定测试
forge test --match-test testTransfer -vv
# 生成覆盖率报告
forge coverage
# 生成 Gas 报告
forge test --gas-report
# 清理缓存
forge clean
```
## 🎯 项目亮点
1. **完整的 ERC20 实现** - 包含所有标准功能
2. **批量转账合约** - 支持批量和代理转账
3. **全面的测试覆盖** - 20+ 测试用例
4. **自动化脚本** - 一键测试和部署
5. **详细的中文文档** - 新手友好
## 🆘 常见问题
### Q: forge 命令找不到?
A: 需要先安装 Foundry,参考第一步。
### Q: 测试失败怎么办?
A: 运行 `forge test -vvvv` 查看详细错误信息。
### Q: 如何部署到测试网?
A: 修改 `.env` 中的私钥和 RPC URL,然后运行部署脚本。
### Q: 本地 RPC 连接失败?
A: 确保 Anvil 正在运行(执行 `anvil` 命令)。
## 📞 获取帮助
- 查看 README.md 获取完整文档
- 查看 [Foundry 官方文档](https://book.getfoundry.sh/)
- 查看合约源码中的注释
# foundrytest # Foundry ERC20 本地测试项目
这是一个使用 Foundry 框架搭建的智能合约测试项目,包含 ERC20 代币合约和内部转账合约的完整实现和测试。
## 📋 项目结构
```
foundrytest/
├── src/ # 合约源代码
│ ├── ERC20Token.sol # ERC20 代币合约
│ └── InternalTransfer.sol # 内部转账合约
├── test/ # 测试文件
│ ├── ERC20Token.t.sol # ERC20 测试用例
│ └── InternalTransfer.t.sol # 内部转账测试用例
├── script/ # 部署脚本
│ ├── DeployERC20.s.sol # 部署 ERC20
│ ├── DeployInternalTransfer.s.sol # 部署内部转账合约
│ └── DeployAll.s.sol # 部署所有合约
├── foundry.toml # Foundry 配置文件
├── .env # 环境变量(本地测试用)
├── test-all.sh # 测试脚本
└── deploy-local.sh # 本地部署脚本
```
## 🔧 合约功能
### 1. ERC20Token.sol
标准 ERC20 代币合约,实现以下功能:
- ✅ 标准 ERC20 接口(transfer, approve, transferFrom)
- ✅ 铸币功能(mint)
- ✅ 代币信息查询(name, symbol, decimals, totalSupply)
- ✅ 余额和授权查询(balanceOf, allowance)
### 2. InternalTransfer.sol
内部转账管理合约,提供批量转账功能:
- ✅ 批量转账(batchTransfer)- 从调用者账户批量转出
- ✅ 代理转账(proxyTransfer)- 代理执行转账(需授权)
- ✅ 批量代理转账(batchProxyTransfer)- 批量代理转账
- ✅ 余额查询(getBalance)
- ✅ 紧急提取(emergencyWithdraw)- 仅owner可用
- ✅ 所有权转移(transferOwnership)
## 🚀 快速开始
### 前置要求
1. **安装 Foundry**
```bash
curl -L https://foundry.paradigm.xyz | bash
foundryup
```
2. **验证安装**
```bash
forge --version
anvil --version
cast --version
```
### 安装依赖
```bash
cd foundrytest
forge install foundry-rs/forge-std --no-commit
```
## 🧪 测试步骤
### 方法一:使用自动化测试脚本(推荐)
直接运行一键测试脚本:
```bash
./test-all.sh
```
该脚本会自动执行:
1. 编译合约
2. 运行所有测试用例
3. 生成测试覆盖率报告
4. 生成 Gas 消耗报告
5. 运行详细测试(包含 console.log 输出)
### 方法二:手动执行测试
#### 1. 编译合约
```bash
forge build
```
#### 2. 运行所有测试
```bash
forge test
```
#### 3. 运行详细测试(显示详细日志)
```bash
forge test -vv # 显示测试名称和结果
forge test -vvv # 显示失败的详细信息
forge test -vvvv # 显示所有信息包括trace
```
#### 4. 运行特定测试文件
```bash
# 只测试 ERC20
forge test --match-path test/ERC20Token.t.sol -vv
# 只测试 InternalTransfer
forge test --match-path test/InternalTransfer.t.sol -vv
```
#### 5. 运行特定测试函数
```bash
forge test --match-test testTransfer -vv
forge test --match-test testBatchTransfer -vv
```
#### 6. 生成测试覆盖率报告
```bash
forge coverage
```
#### 7. 生成 Gas 报告
```bash
forge test --gas-report
```
#### 8. 运行快照测试(Gas 基准测试)
```bash
forge snapshot
```
## 🌐 本地 RPC 部署测试
### 步骤 1:启动本地 Anvil 节点
在一个终端窗口中启动 Anvil(Foundry 的本地测试网络):
```bash
anvil
```
Anvil 会启动一个本地 RPC 服务器在 `http://127.0.0.1:8545`,并提供 10 个测试账户及其私钥。
### 步骤 2:部署合约到本地网络
在另一个终端窗口中,运行部署脚本:
```bash
./deploy-local.sh
```
或者手动执行部署:
```bash
# 部署所有合约
forge script script/DeployAll.s.sol:DeployAll \
--rpc-url http://127.0.0.1:8545 \
--broadcast \
-vvvv
# 或者单独部署 ERC20
forge script script/DeployERC20.s.sol:DeployERC20 \
--rpc-url http://127.0.0.1:8545 \
--broadcast
# 或者单独部署 InternalTransfer
forge script script/DeployInternalTransfer.s.sol:DeployInternalTransfer \
--rpc-url http://127.0.0.1:8545 \
--broadcast
```
### 步骤 3:查看部署结果
部署完成后,合约地址会保存在 `broadcast/` 目录下:
```bash
cat broadcast/DeployAll.s.sol/31337/run-latest.json
```
## 🔍 与合约交互
部署后可以使用 `cast` 命令与合约交互:
### 查询代币信息
```bash
# 设置合约地址(替换为实际部署的地址)
TOKEN_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3
# 查询代币名称
cast call $TOKEN_ADDRESS "name()(string)"
# 查询代币符号
cast call $TOKEN_ADDRESS "symbol()(string)"
# 查询总供应量
cast call $TOKEN_ADDRESS "totalSupply()(uint256)"
# 查询余额
cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
```
### 执行转账
```bash
# 转账代币
cast send $TOKEN_ADDRESS \
"transfer(address,uint256)(bool)" \
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
"1000000000000000000000" \
--rpc-url http://127.0.0.1:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
```
### 批量转账
```bash
TRANSFER_ADDRESS=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
# 1. 先授权
cast send $TOKEN_ADDRESS \
"approve(address,uint256)(bool)" \
$TRANSFER_ADDRESS \
"1000000000000000000000" \
--rpc-url http://127.0.0.1:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
# 2. 执行批量转账
cast send $TRANSFER_ADDRESS \
"batchTransfer(address,address[],uint256[])(bool)" \
$TOKEN_ADDRESS \
"[0x70997970C51812dc3A010C7d01b50e0d17dc79C8,0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC]" \
"[100000000000000000000,200000000000000000000]" \
--rpc-url http://127.0.0.1:8545 \
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
```
## 📊 测试用例说明
### ERC20Token 测试用例
-`testInitialSupply` - 测试初始供应量
-`testTokenInfo` - 测试代币信息
-`testTransfer` - 测试基本转账
-`testTransferFailInsufficientBalance` - 测试余额不足的转账
-`testTransferToZeroAddress` - 测试转账到零地址
-`testApprove` - 测试授权
-`testTransferFrom` - 测试授权转账
-`testTransferFromFailInsufficientAllowance` - 测试授权不足
-`testMint` - 测试铸币
-`testMintToZeroAddress` - 测试铸币到零地址
### InternalTransfer 测试用例
-`testBatchTransfer` - 测试批量转账
-`testBatchTransferFailLengthMismatch` - 测试数组长度不匹配
-`testBatchTransferFailEmptyRecipients` - 测试空接收者列表
-`testProxyTransfer` - 测试代理转账
-`testProxyTransferFailNoApproval` - 测试无授权的代理转账
-`testBatchProxyTransfer` - 测试批量代理转账
-`testGetBalance` - 测试余额查询
-`testEmergencyWithdraw` - 测试紧急提取
-`testEmergencyWithdrawFailNotOwner` - 测试非owner紧急提取
-`testTransferOwnership` - 测试所有权转移
-`testTransferOwnershipFailNotOwner` - 测试非owner转移所有权
-`testEvents` - 测试事件发送
## 🛠️ 常用命令
```bash
# 清理构建缓存
forge clean
# 格式化代码
forge fmt
# 检查代码格式
forge fmt --check
# 查看帮助
forge --help
forge test --help
forge script --help
# 更新依赖
forge update
# 安装新的依赖
forge install <dependency>
```
## 🔐 安全说明
⚠️ **重要提示**
- `.env` 文件中的私钥仅用于本地测试(Anvil 默认账户)
- 切勿在主网或测试网使用这些私钥
- 生产环境请使用硬件钱包或安全的密钥管理方案
- `.env` 文件已添加到 `.gitignore`,请勿提交到版本控制
## 📚 相关资源
- [Foundry 官方文档](https://book.getfoundry.sh/)
- [Foundry GitHub](https://github.com/foundry-rs/foundry)
- [Solidity 文档](https://docs.soliditylang.org/)
- [OpenZeppelin 合约库](https://docs.openzeppelin.com/contracts/)
## 📝 开发建议
1. **编写测试前先思考边界情况**
2. **使用 `forge test -vvvv` 调试失败的测试**
3. **定期运行 `forge coverage` 检查测试覆盖率**
4. **使用 `forge snapshot` 监控 Gas 消耗变化**
5. **部署前确保所有测试通过**
## 🎯 项目特点
- ✅ 完整的 ERC20 实现
- ✅ 实用的批量转账功能
- ✅ 全面的测试覆盖
- ✅ 本地 RPC 测试环境
- ✅ 自动化测试脚本
- ✅ 详细的中文文档
## 📄 许可证
MIT License
#!/bin/bash
# 颜色定义
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${BLUE}================================${NC}"
echo -e "${BLUE} RPC部署脚本${NC}"
echo -e "${BLUE}================================${NC}"
echo ""
# 检查.env文件是否存在
if [ ! -f .env ]; then
echo -e "${RED}.env 文件不存在!${NC}"
echo -e "${YELLOW}请创建.env文件并添加以下内容:${NC}"
echo "PRIVATE_KEY=your_private_key_here"
echo "RPC_URL=http://127.0.0.1:8545"
exit 1
fi
# 加载环境变量
source .env
# 设置默认RPC URL
if [ -z "$RPC_URL" ]; then
RPC_URL="http://127.0.0.1:8545"
fi
echo -e "${GREEN}使用 RPC: $RPC_URL${NC}"
echo ""
# 检查RPC是否可用
echo -e "${GREEN}[1/4] 检查RPC连接...${NC}"
curl -s -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
$RPC_URL > /dev/null
if [ $? -ne 0 ]; then
echo -e "${RED}无法连接到RPC ($RPC_URL)${NC}"
if [ "$RPC_URL" == "http://127.0.0.1:8545" ]; then
echo -e "${YELLOW}请先运行: anvil${NC}"
else
echo -e "${YELLOW}请检查RPC URL是否正确${NC}"
fi
exit 1
fi
echo -e "${GREEN}✓ RPC连接成功${NC}"
echo ""
# 编译合约
echo -e "${GREEN}[2/4] 编译合约...${NC}"
forge build
if [ $? -ne 0 ]; then
echo -e "${RED}编译失败!${NC}"
exit 1
fi
echo -e "${GREEN}✓ 编译成功${NC}"
echo ""
# 部署所有合约
echo -e "${GREEN}[3/4] 部署合约到 $RPC_URL...${NC}"
forge script script/DeployAll.s.sol:DeployAll \
--rpc-url $RPC_URL \
--broadcast \
-vvvv
if [ $? -ne 0 ]; then
echo -e "${RED}部署失败!${NC}"
exit 1
fi
echo -e "${GREEN}✓ 部署成功${NC}"
echo ""
# 显示部署信息
echo -e "${GREEN}[4/4] 部署信息${NC}"
echo -e "${YELLOW}查看 broadcast/ 目录获取详细的部署地址${NC}"
echo ""
echo -e "${BLUE}================================${NC}"
echo -e "${GREEN}部署完成!${NC}"
echo -e "${BLUE}================================${NC}"
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.20"
[rpc_endpoints]
local = "http://127.0.0.1:8545"
[etherscan]
mainnet = { key = "${ETHERSCAN_API_KEY}" }
console.log("");
// 3. 进行一些初始化操作
address testUser = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; // 测试账户
uint256 transferAmount = 10000 * 10**18;
// 给测试账户转一些代币
token.transfer(testUser, transferAmount);
console.log("=== Initial Setup ===");
console.log("Transferred", transferAmount, "tokens to", testUser);
console.log("Test user balance:", token.balanceOf(testUser));
vm.stopBroadcast();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import "../src/ERC20Token.sol";
import "../src/InternalTransfer.sol";
contract DeployAll is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// 1. 部署ERC20代币
ERC20Token token = new ERC20Token("Test Token", "TST", 18, 1000000);
console.log("=== ERC20Token Deployed ===");
console.log("Address:", address(token));
console.log("Name:", token.name());
console.log("Symbol:", token.symbol());
console.log("Total Supply:", token.totalSupply());
console.log("");
// 2. 部署内部转账合约
InternalTransfer transferContract = new InternalTransfer();
console.log("=== InternalTransfer Deployed ===");
console.log("Address:", address(transferContract));
console.log("Owner:", transferContract.owner());
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import "../src/ERC20Token.sol";
contract DeployERC20 is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// 部署ERC20代币,初始供应量1000000
ERC20Token token = new ERC20Token("Test Token", "TST", 18, 1000000);
console.log("ERC20Token deployed at:", address(token));
console.log("Total Supply:", token.totalSupply());
console.log("Deployer Balance:", token.balanceOf(msg.sender));
vm.stopBroadcast();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Script.sol";
import "../src/InternalTransfer.sol";
contract DeployInternalTransfer is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
// 部署内部转账合约
InternalTransfer transferContract = new InternalTransfer();
console.log("InternalTransfer deployed at:", address(transferContract));
console.log("Owner:", transferContract.owner());
vm.stopBroadcast();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title ERC20Token
* @dev 实现基本的ERC20代币合约
*/
contract ERC20Token {
string public name;
string public symbol;
uint8 public decimals;
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, uint8 _decimals, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
decimals = _decimals;
totalSupply = _initialSupply * 10 ** uint256(_decimals);
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(_to != address(0), "ERC20: transfer to the zero address");
require(balanceOf[msg.sender] >= _value, "ERC20: insufficient balance");
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
function approve(address _spender, uint256 _value) public returns (bool success) {
require(_spender != address(0), "ERC20: approve to the zero address");
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(_from != address(0), "ERC20: transfer from the zero address");
require(_to != address(0), "ERC20: transfer to the zero address");
require(balanceOf[_from] >= _value, "ERC20: insufficient balance");
require(allowance[_from][msg.sender] >= _value, "ERC20: insufficient allowance");
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
allowance[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
function mint(address _to, uint256 _amount) public returns (bool) {
require(_to != address(0), "ERC20: mint to the zero address");
totalSupply += _amount;
balanceOf[_to] += _amount;
emit Transfer(address(0), _to, _amount);
return true;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
}
/**
* @title InternalTransfer
* @dev 内部转账合约,支持批量转账和代理转账功能
*/
contract InternalTransfer {
address public owner;
event BatchTransfer(address indexed token, address indexed from, uint256 totalAmount, uint256 recipientCount);
event ProxyTransfer(address indexed token, address indexed from, address indexed to, uint256 amount);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor() {
owner = msg.sender;
}
/**
* @dev 批量转账 - 从调用者账户转出
* @param token ERC20代币地址
* @param recipients 接收者地址数组
* @param amounts 转账金额数组
*/
function batchTransfer(
address token,
address[] calldata recipients,
uint256[] calldata amounts
) external returns (bool) {
require(recipients.length == amounts.length, "Length mismatch");
require(recipients.length > 0, "Empty recipients");
IERC20 tokenContract = IERC20(token);
uint256 totalAmount = 0;
for (uint256 i = 0; i < recipients.length; i++) {
require(recipients[i] != address(0), "Invalid recipient");
require(amounts[i] > 0, "Invalid amount");
totalAmount += amounts[i];
}
// 先将总金额从调用者转到合约
require(
tokenContract.transferFrom(msg.sender, address(this), totalAmount),
"Transfer to contract failed"
);
// 再从合约分发到各个接收者
for (uint256 i = 0; i < recipients.length; i++) {
require(
tokenContract.transfer(recipients[i], amounts[i]),
"Transfer to recipient failed"
);
}
emit BatchTransfer(token, msg.sender, totalAmount, recipients.length);
return true;
}
/**
* @dev 代理转账 - 从指定地址转账到另一个地址
* @param token ERC20代币地址
* @param from 转出地址
* @param to 接收地址
* @param amount 转账金额
*/
function proxyTransfer(
address token,
address from,
address to,
uint256 amount
) external returns (bool) {
require(from != address(0), "Invalid from address");
require(to != address(0), "Invalid to address");
require(amount > 0, "Invalid amount");
IERC20 tokenContract = IERC20(token);
// 使用transferFrom从from地址转账到to地址
require(
tokenContract.transferFrom(from, to, amount),
"Proxy transfer failed"
);
emit ProxyTransfer(token, from, to, amount);
return true;
}
/**
* @dev 批量代理转账
* @param token ERC20代币地址
* @param from 统一的转出地址
* @param recipients 接收者地址数组
* @param amounts 转账金额数组
*/
function batchProxyTransfer(
address token,
address from,
address[] calldata recipients,
uint256[] calldata amounts
) external returns (bool) {
require(recipients.length == amounts.length, "Length mismatch");
require(recipients.length > 0, "Empty recipients");
require(from != address(0), "Invalid from address");
IERC20 tokenContract = IERC20(token);
uint256 totalAmount = 0;
for (uint256 i = 0; i < recipients.length; i++) {
require(recipients[i] != address(0), "Invalid recipient");
require(amounts[i] > 0, "Invalid amount");
require(
tokenContract.transferFrom(from, recipients[i], amounts[i]),
"Proxy transfer failed"
);
totalAmount += amounts[i];
}
emit BatchTransfer(token, from, totalAmount, recipients.length);
return true;
}
/**
* @dev 查询代币余额
* @param token ERC20代币地址
* @param account 查询地址
*/
function getBalance(address token, address account) external view returns (uint256) {
return IERC20(token).balanceOf(account);
}
/**
* @dev 紧急提取代币(仅owner)
* @param token 代币地址
* @param amount 提取金额
*/
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
require(IERC20(token).transfer(owner, amount), "Withdraw failed");
}
/**
* @dev 转移所有权
*/
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "Invalid new owner");
owner = newOwner;
}
}
#!/bin/bash
# 颜色定义
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${BLUE}================================${NC}"
echo -e "${BLUE} Foundry 测试脚本${NC}"
echo -e "${BLUE}================================${NC}"
echo ""
# 加载环境变量(如果存在)
if [ -f .env ]; then
source .env
if [ ! -z "$RPC_URL" ] && [ "$RPC_URL" != "http://127.0.0.1:8545" ]; then
echo -e "${YELLOW}使用自定义 RPC: $RPC_URL${NC}"
echo ""
fi
fi
# 1. 编译合约
echo -e "${GREEN}[1/5] 编译智能合约...${NC}"
forge build
if [ $? -ne 0 ]; then
echo -e "${RED}编译失败!${NC}"
exit 1
fi
echo -e "${GREEN}✓ 编译成功${NC}"
echo ""
# 2. 运行测试
echo -e "${GREEN}[2/5] 运行测试用例...${NC}"
if [ ! -z "$RPC_URL" ] && [ "$RPC_URL" != "http://127.0.0.1:8545" ]; then
forge test --fork-url $RPC_URL -vv
else
forge test -vv
fi
if [ $? -ne 0 ]; then
echo -e "${RED}测试失败!${NC}"
exit 1
fi
echo -e "${GREEN}✓ 测试通过${NC}"
echo ""
# 3. 生成测试覆盖率报告
echo -e "${GREEN}[3/5] 生成测试覆盖率报告...${NC}"
if [ ! -z "$RPC_URL" ] && [ "$RPC_URL" != "http://127.0.0.1:8545" ]; then
forge coverage --fork-url $RPC_URL
else
forge coverage
fi
echo -e "${GREEN}✓ 覆盖率报告生成完成${NC}"
echo ""
# 4. 运行Gas报告
echo -e "${GREEN}[4/5] 生成Gas消耗报告...${NC}"
if [ ! -z "$RPC_URL" ] && [ "$RPC_URL" != "http://127.0.0.1:8545" ]; then
forge test --fork-url $RPC_URL --gas-report
else
forge test --gas-report
fi
echo -e "${GREEN}✓ Gas报告生成完成${NC}"
echo ""
# 5. 运行详细测试(显示console.log输出)
echo -e "${GREEN}[5/5] 运行详细测试...${NC}"
if [ ! -z "$RPC_URL" ] && [ "$RPC_URL" != "http://127.0.0.1:8545" ]; then
forge test --fork-url $RPC_URL -vvv
else
forge test -vvv
fi
echo -e "${GREEN}✓ 详细测试完成${NC}"
echo ""
echo -e "${BLUE}================================${NC}"
echo -e "${GREEN}所有测试完成!${NC}"
echo -e "${BLUE}================================${NC}"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/ERC20Token.sol";
contract ERC20TokenTest is Test {
ERC20Token public token;
address public owner;
address public user1;
address public user2;
function setUp() public {
owner = address(this);
user1 = address(0x1);
user2 = address(0x2);
// 部署代币合约,初始供应量1000000
token = new ERC20Token("Test Token", "TST", 18, 1000000);
}
function testInitialSupply() public {
assertEq(token.totalSupply(), 1000000 * 10**18);
assertEq(token.balanceOf(owner), 1000000 * 10**18);
}
function testTokenInfo() public {
assertEq(token.name(), "Test Token");
assertEq(token.symbol(), "TST");
assertEq(token.decimals(), 18);
}
function testTransfer() public {
uint256 amount = 1000 * 10**18;
assertTrue(token.transfer(user1, amount));
assertEq(token.balanceOf(user1), amount);
assertEq(token.balanceOf(owner), 1000000 * 10**18 - amount);
}
function testTransferFailInsufficientBalance() public {
uint256 amount = 1000 * 10**18;
vm.prank(user1);
vm.expectRevert("ERC20: insufficient balance");
token.transfer(user2, amount);
}
function testTransferToZeroAddress() public {
uint256 amount = 1000 * 10**18;
vm.expectRevert("ERC20: transfer to the zero address");
token.transfer(address(0), amount);
}
function testApprove() public {
uint256 amount = 1000 * 10**18;
assertTrue(token.approve(user1, amount));
assertEq(token.allowance(owner, user1), amount);
}
function testTransferFrom() public {
uint256 amount = 1000 * 10**18;
// owner批准user1可以花费amount数量的代币
token.approve(user1, amount);
// user1从owner转账到user2
vm.prank(user1);
assertTrue(token.transferFrom(owner, user2, amount));
assertEq(token.balanceOf(user2), amount);
assertEq(token.allowance(owner, user1), 0);
}
function testTransferFromFailInsufficientAllowance() public {
uint256 amount = 1000 * 10**18;
vm.prank(user1);
vm.expectRevert("ERC20: insufficient allowance");
token.transferFrom(owner, user2, amount);
}
function testMint() public {
uint256 mintAmount = 1000 * 10**18;
uint256 initialSupply = token.totalSupply();
assertTrue(token.mint(user1, mintAmount));
assertEq(token.balanceOf(user1), mintAmount);
assertEq(token.totalSupply(), initialSupply + mintAmount);
}
function testMintToZeroAddress() public {
vm.expectRevert("ERC20: mint to the zero address");
token.mint(address(0), 1000);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/ERC20Token.sol";
import "../src/InternalTransfer.sol";
contract InternalTransferTest is Test {
ERC20Token public token;
InternalTransfer public transferContract;
address public owner;
address public user1;
address public user2;
address public user3;
function setUp() public {
owner = address(this);
user1 = address(0x1);
user2 = address(0x2);
user3 = address(0x3);
// 部署代币和转账合约
token = new ERC20Token("Test Token", "TST", 18, 1000000);
transferContract = new InternalTransfer();
// 给user1一些代币用于测试
token.transfer(user1, 10000 * 10**18);
}
function testBatchTransfer() public {
uint256 amount1 = 100 * 10**18;
uint256 amount2 = 200 * 10**18;
uint256 totalAmount = amount1 + amount2;
address[] memory recipients = new address[](2);
recipients[0] = user2;
recipients[1] = user3;
uint256[] memory amounts = new uint256[](2);
amounts[0] = amount1;
amounts[1] = amount2;
// user1批准转账合约
vm.prank(user1);
token.approve(address(transferContract), totalAmount);
// 执行批量转账
vm.prank(user1);
assertTrue(transferContract.batchTransfer(address(token), recipients, amounts));
// 验证余额
assertEq(token.balanceOf(user2), amount1);
assertEq(token.balanceOf(user3), amount2);
}
function testBatchTransferFailLengthMismatch() public {
address[] memory recipients = new address[](2);
recipients[0] = user2;
recipients[1] = user3;
uint256[] memory amounts = new uint256[](1);
amounts[0] = 100 * 10**18;
vm.prank(user1);
vm.expectRevert("Length mismatch");
transferContract.batchTransfer(address(token), recipients, amounts);
}
function testBatchTransferFailEmptyRecipients() public {
address[] memory recipients = new address[](0);
uint256[] memory amounts = new uint256[](0);
vm.prank(user1);
vm.expectRevert("Empty recipients");
transferContract.batchTransfer(address(token), recipients, amounts);
}
function testProxyTransfer() public {
uint256 amount = 500 * 10**18;
// user1批准转账合约
vm.prank(user1);
token.approve(address(transferContract), amount);
// 任何人都可以调用代理转账(只要有授权)
assertTrue(transferContract.proxyTransfer(address(token), user1, user2, amount));
// 验证余额
assertEq(token.balanceOf(user2), amount);
assertEq(token.balanceOf(user1), 10000 * 10**18 - amount);
}
function testProxyTransferFailNoApproval() public {
uint256 amount = 500 * 10**18;
vm.expectRevert("Proxy transfer failed");
transferContract.proxyTransfer(address(token), user1, user2, amount);
}
function testBatchProxyTransfer() public {
uint256 amount1 = 100 * 10**18;
uint256 amount2 = 200 * 10**18;
uint256 totalAmount = amount1 + amount2;
address[] memory recipients = new address[](2);
recipients[0] = user2;
recipients[1] = user3;
uint256[] memory amounts = new uint256[](2);
amounts[0] = amount1;
amounts[1] = amount2;
// user1批准转账合约
vm.prank(user1);
token.approve(address(transferContract), totalAmount);
// 执行批量代理转账
assertTrue(transferContract.batchProxyTransfer(address(token), user1, recipients, amounts));
// 验证余额
assertEq(token.balanceOf(user2), amount1);
assertEq(token.balanceOf(user3), amount2);
}
function testGetBalance() public {
uint256 balance = transferContract.getBalance(address(token), user1);
assertEq(balance, 10000 * 10**18);
}
function testEmergencyWithdraw() public {
uint256 amount = 1000 * 10**18;
// 先给合约转一些代币
token.transfer(address(transferContract), amount);
uint256 ownerBalanceBefore = token.balanceOf(owner);
// owner提取代币
transferContract.emergencyWithdraw(address(token), amount);
assertEq(token.balanceOf(owner), ownerBalanceBefore + amount);
assertEq(token.balanceOf(address(transferContract)), 0);
}
function testEmergencyWithdrawFailNotOwner() public {
uint256 amount = 1000 * 10**18;
token.transfer(address(transferContract), amount);
vm.prank(user1);
vm.expectRevert("Not owner");
transferContract.emergencyWithdraw(address(token), amount);
}
function testTransferOwnership() public {
transferContract.transferOwnership(user1);
assertEq(transferContract.owner(), user1);
}
function testTransferOwnershipFailNotOwner() public {
vm.prank(user1);
vm.expectRevert("Not owner");
transferContract.transferOwnership(user2);
}
function testEvents() public {
uint256 amount1 = 100 * 10**18;
uint256 amount2 = 200 * 10**18;
address[] memory recipients = new address[](2);
recipients[0] = user2;
recipients[1] = user3;
uint256[] memory amounts = new uint256[](2);
amounts[0] = amount1;
amounts[1] = amount2;
vm.prank(user1);
token.approve(address(transferContract), amount1 + amount2);
vm.prank(user1);
vm.expectEmit(true, true, false, true);
emit InternalTransfer.BatchTransfer(address(token), user1, amount1 + amount2, 2);
transferContract.batchTransfer(address(token), recipients, amounts);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment