手动审计练习#2 - 审计一个DeFi合约(Vault/Staking)
### 1. DeFi合约审计 vs Token合约审计
日期: 2026-06-17 方向: Solidity / Audit 阶段: 第三阶段:安全审计 标签: #defi-audit #vault #staking #share-calculation #flash-loan-attack #first-depositor
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 掌握DeFi特有的审计关注点:份额计算、首存者攻击、闪电贷交互、奖励分配 |
| 实操 | 对一个包含多个DeFi漏洞的Vault合约进行完整手动审计 |
| 产出 | 一份DeFi审计报告 + DeFi常见漏洞速查清单 |
核心概念
1. DeFi合约审计 vs Token合约审计
DeFi合约(Vault、Staking、Lending等)比普通Token合约复杂得多。审计时需要额外关注:
| 关注点 | 说明 | 典型漏洞 |
|---|---|---|
| 份额计算(Share Calculation) | 存入资产→铸造份额,取出资产→销毁份额 | 舍入误差、首存者攻击 |
| 奖励分配(Reward Distribution) | 按份额比例分配奖励 | 精度丢失、奖励偷取 |
| 闪电贷交互(Flash Loan) | 合约是否能被闪电贷利用 | 价格操纵、瞬时余额膨胀 |
| 重入风险(Reentrancy) | 外部调用+状态修改 | 经典重入、跨函数重入 |
| 价格依赖(Price Oracle) | 依赖外部价格源 | 预言机操纵、延迟攻击 |
| 时间依赖(Timing) | 基于区块时间的逻辑 | 时间操纵、抢跑 |
2. Vault份额计算原理
ERC-4626标准的核心公式:
存入时铸造的份额:
shares = (depositAmount * totalShares) / totalAssets
取出时销毁的份额:
assets = (shares * totalAssets) / totalShares
为什么这个公式有问题?
当totalShares或totalAssets为0时(首次存入),除法会出错。且由于Solidity整数除法向下截断,小数部分会被丢弃,这在特定条件下可以被利用。
3. 首存者攻击(First Depositor Attack / Inflation Attack)
这是DeFi Vault中最经典的攻击之一。攻击步骤:
初始状态:Vault为空,totalAssets=0, totalShares=0
Step 1: 攻击者Alice存入1 wei → 获得1 share
totalAssets=1, totalShares=1
Step 2: Alice直接转入大量Token到Vault合约(不通过deposit)
例如转入 10000e18
totalAssets=10000e18+1, totalShares=1
Step 3: 受害者Bob存入 19999e18
Bob的shares = (19999e18 * 1) / (10000e18 + 1) = 1(向下截断为1)
totalAssets=29999e18+1, totalShares=2
Step 4: Alice取出1 share
Alice得到 = (1 * (29999e18+1)) / 2 ≈ 14999.5e18
结果:Alice投入约10000e18,取出约14999.5e18,净赚约4999.5e18
Bob投入19999e18,只剩1 share能取出约14999.5e18,亏了约4999.5e18
防御方式:
- 首次存入时铸造额外份额到dead address(如OpenZeppelin的ERC4626实现)
- 设置最小存入量
- 使用虚拟偏移(virtual offset):在计算中始终加一个小数
4. 奖励分配的精度问题
经典的 rewardPerShare 累加模式:
// 每次新奖励到达时更新
rewardPerShare += newReward * PRECISION / totalShares;
// 用户领取时计算
userReward = (user.shares * rewardPerShare / PRECISION) - user.rewardDebt;
如果 PRECISION 不够大,或者 totalShares 远大于 newReward,rewardPerShare 的增量可能被截断为0,导致奖励"消失"。
代码实战
待审计合约:MomoVault
以下是一个Staking Vault合约,用户存入ERC20 Token获得份额(shares),按份额获取Staking奖励。包含6+个安全问题。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
/**
* @title MomoVault
* @notice A staking vault where users deposit tokens and earn rewards.
* @dev Users deposit stakingToken, receive shares proportional to their deposit.
* Admin can add rewards which are distributed proportionally to shareholders.
*/
contract MomoVault {
IERC20 public stakingToken;
address public admin;
uint256 public totalShares;
uint256 public totalStaked;
// Reward tracking
uint256 public rewardPerShare; // accumulated reward per share (scaled by 1e18)
uint256 public lastRewardTime;
uint256 public rewardRate; // reward tokens per second
struct UserInfo {
uint256 shares;
uint256 rewardDebt;
uint256 depositTime;
}
mapping(address => UserInfo) public userInfo;
// Withdrawal fee: 1% if withdrawn within 7 days
uint256 public constant EARLY_WITHDRAWAL_FEE = 100; // 1% in basis points
uint256 public constant LOCK_PERIOD = 7 days;
uint256 public constant FEE_DENOMINATOR = 10000;
event Deposit(address indexed user, uint256 amount, uint256 shares);
event Withdraw(address indexed user, uint256 amount, uint256 shares);
event RewardClaimed(address indexed user, uint256 reward);
event RewardRateUpdated(uint256 newRate);
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
constructor(address _stakingToken) {
stakingToken = IERC20(_stakingToken);
admin = msg.sender;
lastRewardTime = block.timestamp;
}
/// @notice Update the accumulated reward per share
function updateRewards() public {
if (totalShares == 0) {
lastRewardTime = block.timestamp;
return;
}
uint256 timeElapsed = block.timestamp - lastRewardTime;
uint256 reward = timeElapsed * rewardRate;
rewardPerShare += (reward * 1e18) / totalShares;
lastRewardTime = block.timestamp;
}
/// @notice Deposit tokens and receive shares
function deposit(uint256 amount) external {
require(amount > 0, "Cannot deposit 0");
updateRewards();
// Calculate shares
uint256 shares;
if (totalShares == 0) {
shares = amount;
} else {
shares = (amount * totalShares) / totalStaked;
}
// Transfer tokens from user
stakingToken.transferFrom(msg.sender, address(this), amount);
// Update state
UserInfo storage user = userInfo[msg.sender];
user.shares += shares;
user.rewardDebt = (user.shares * rewardPerShare) / 1e18;
user.depositTime = block.timestamp;
totalShares += shares;
totalStaked += amount;
emit Deposit(msg.sender, amount, shares);
}
/// @notice Withdraw tokens by burning shares
function withdraw(uint256 sharesToBurn) external {
UserInfo storage user = userInfo[msg.sender];
require(user.shares >= sharesToBurn, "Insufficient shares");
require(sharesToBurn > 0, "Cannot withdraw 0");
updateRewards();
// Claim pending rewards first
uint256 pending = (user.shares * rewardPerShare) / 1e18 - user.rewardDebt;
if (pending > 0) {
stakingToken.transfer(msg.sender, pending);
}
// Calculate withdrawal amount
uint256 amount = (sharesToBurn * totalStaked) / totalShares;
// Apply early withdrawal fee
if (block.timestamp < user.depositTime + LOCK_PERIOD) {
uint256 fee = (amount * EARLY_WITHDRAWAL_FEE) / FEE_DENOMINATOR;
amount -= fee;
// Fee stays in the vault (benefits remaining stakers)
}
// Update state
user.shares -= sharesToBurn;
user.rewardDebt = (user.shares * rewardPerShare) / 1e18;
totalShares -= sharesToBurn;
totalStaked -= amount;
// Transfer tokens to user
stakingToken.transfer(msg.sender, amount);
emit Withdraw(msg.sender, amount, sharesToBurn);
}
/// @notice Claim pending rewards without withdrawing
function claimReward() external {
updateRewards();
UserInfo storage user = userInfo[msg.sender];
uint256 pending = (user.shares * rewardPerShare) / 1e18 - user.rewardDebt;
if (pending > 0) {
user.rewardDebt = (user.shares * rewardPerShare) / 1e18;
stakingToken.transfer(msg.sender, pending);
emit RewardClaimed(msg.sender, pending);
}
}
/// @notice Admin sets the reward rate
function setRewardRate(uint256 _rewardRate) external onlyAdmin {
updateRewards();
rewardRate = _rewardRate;
emit RewardRateUpdated(_rewardRate);
}
/// @notice Admin can deposit reward tokens
function depositRewards(uint256 amount) external onlyAdmin {
stakingToken.transferFrom(msg.sender, address(this), amount);
}
/// @notice Emergency withdraw - forfeit all rewards
function emergencyWithdraw() external {
UserInfo storage user = userInfo[msg.sender];
uint256 shares = user.shares;
require(shares > 0, "No shares");
uint256 amount = (shares * totalStaked) / totalShares;
user.shares = 0;
user.rewardDebt = 0;
totalShares -= shares;
totalStaked -= amount;
stakingToken.transfer(msg.sender, amount);
emit Withdraw(msg.sender, amount, shares);
}
/// @notice View pending rewards
function pendingReward(address _user) external view returns (uint256) {
UserInfo storage user = userInfo[_user];
uint256 accRewardPerShare = rewardPerShare;
if (totalShares > 0 && block.timestamp > lastRewardTime) {
uint256 timeElapsed = block.timestamp - lastRewardTime;
uint256 reward = timeElapsed * rewardRate;
accRewardPerShare += (reward * 1e18) / totalShares;
}
return (user.shares * accRewardPerShare) / 1e18 - user.rewardDebt;
}
}
完整审计报告
审计概要
| 项目 | 详情 |
|---|---|
| 合约名 | MomoVault |
| 审计范围 | MomoVault.sol (单文件) |
| Solidity版本 | ^0.8.20 |
| 审计日期 | 2026-06-17 |
| 合约类型 | Staking Vault with Reward Distribution |
发现汇总
| 编号 | 标题 | 严重性 |
|---|---|---|
| H-01 | 首存者攻击(Inflation Attack)可窃取后续用户资金 | High |
| H-02 | 奖励分配不足时的资金亏空(Insolvency Risk) | High |
| H-03 | withdraw中totalStaked扣减金额错误,导致accounting偏差 | High |
| M-01 | ERC20 transfer返回值未检查 | Medium |
| M-02 | deposit中rewardDebt覆盖导致已有奖励丢失 | Medium |
| M-03 | depositTime每次deposit时被覆盖,锁定期可被重置 | Medium |
| L-01 | emergencyWithdraw未调用updateRewards | Low |
| L-02 | 缺少admin转移功能和zero address检查 | Low |
| I-01 | rewardRate设置缺少上限检查 | Informational |
H-01: 首存者攻击(Inflation Attack)
严重性: High 文件: MomoVault.sol 行号: L81-L86
描述: 当Vault为空时,首个存款者可以执行经典的Inflation Attack。
攻击流程:
1. Vault为空,totalShares=0, totalStaked=0
2. Attacker调用 deposit(1),获得1 share
→ totalShares=1, totalStaked=1
3. Attacker直接调用 stakingToken.transfer(vault, 10000e18)
→ Vault余额增加但totalStaked不变(totalStaked仍为1)
注意:本合约使用totalStaked而非balanceOf追踪资产,
所以直接转入不影响totalStaked。但这仍然是一个风险:
如果合约改为使用balanceOf,就会有首存者攻击问题。
更重要的是,即使不直接转账,攻击者可以在deposit(1)后
等待奖励累积(rewardPerShare增长),此时只有1 share,
所有奖励归攻击者。然后后续depositor按照膨胀后的
totalStaked计算shares,获得很少的shares。
但这个合约有一个更直接的问题:当 totalShares == 0 时,shares = amount。如果第一个用户存入1 wei,获得1 share。第二个用户存入很大金额,由于 shares = (amount * 1) / 1 = amount,看起来正常。但如果在两次deposit之间,totalStaked因为奖励等原因增加了,份额计算就会偏离。
影响:
- 后续存款者可能获得比预期少的份额
- 在极端情况下,后续存款者可能获得0 share(所有资金被首存者窃取)
推荐修复:
uint256 public constant MINIMUM_SHARES = 1000;
function deposit(uint256 amount) external {
// ...
uint256 shares;
if (totalShares == 0) {
shares = amount - MINIMUM_SHARES;
// Mint MINIMUM_SHARES to dead address to prevent inflation attack
totalShares += MINIMUM_SHARES;
// These shares are permanently locked
} else {
shares = (amount * totalShares) / totalStaked;
}
require(shares > 0, "Shares must be > 0");
// ...
}
H-02: 奖励分配不足时的资金亏空
严重性: High 文件: MomoVault.sol 行号: L73-L79, L143-L152
描述:
updateRewards 按时间和 rewardRate 计算奖励,但没有检查Vault中是否有足够的Token来支付这些奖励。rewardRate 的设置也没有与实际可用奖励Token绑定。
场景:
1. Admin设置 rewardRate = 100e18(每秒100 Token)
2. Admin只存入了1000e18作为奖励
3. 10秒后,承诺的奖励 = 1000e18(刚好够)
4. 20秒后,承诺的奖励 = 2000e18(但只有1000e18可用)
5. 先领取的用户拿到奖励,后领取的用户的transfer会失败
这导致了"先到先得"的不公平问题,且最后的用户可能完全无法提取。
影响:
- Vault可能承诺了比实际可用余额更多的奖励
- 最后的提取者将面临transfer失败
- 如果stakingToken有缺陷(transfer失败时不revert),用户可能拿不到任何Token
推荐修复:
uint256 public rewardEndTime;
uint256 public totalRewardsAvailable;
function setRewardRate(uint256 _rewardRate, uint256 duration) external onlyAdmin {
updateRewards();
rewardRate = _rewardRate;
rewardEndTime = block.timestamp + duration;
uint256 requiredRewards = _rewardRate * duration;
require(
stakingToken.balanceOf(address(this)) - totalStaked >= requiredRewards,
"Insufficient reward balance"
);
}
function updateRewards() public {
if (totalShares == 0) {
lastRewardTime = block.timestamp;
return;
}
uint256 endTime = block.timestamp < rewardEndTime ? block.timestamp : rewardEndTime;
if (endTime <= lastRewardTime) return;
uint256 timeElapsed = endTime - lastRewardTime;
uint256 reward = timeElapsed * rewardRate;
rewardPerShare += (reward * 1e18) / totalShares;
lastRewardTime = endTime;
}
H-03: withdraw中totalStaked扣减金额错误
严重性: High 文件: MomoVault.sol 行号: L117-L121
描述:
当用户在LOCK_PERIOD内提前取出时,会扣除一笔fee,实际发送的amount减少了。但 totalStaked -= amount 使用的是扣除fee后的amount:
// 计算取出金额
uint256 amount = (sharesToBurn * totalStaked) / totalShares;
// 扣除提前取出费用
if (block.timestamp < user.depositTime + LOCK_PERIOD) {
uint256 fee = (amount * EARLY_WITHDRAWAL_FEE) / FEE_DENOMINATOR;
amount -= fee; // amount被修改为扣费后的金额
// Fee stays in the vault
}
// 更新状态
totalShares -= sharesToBurn;
totalStaked -= amount; // BUG: 这里用的是扣费后的amount!
问题:fee留在Vault中("benefits remaining stakers"),但 totalStaked 只减少了 amount - fee。这意味着fee对应的Token存在于Vault中但没有被 totalStaked 追踪。随着更多用户提前取出,totalStaked 会越来越低于实际余额。
更严重的后果:最后一批用户取出时,(sharesToBurn * totalStaked) / totalShares 的值会低于他们应得的金额,因为 totalStaked 比实际可用余额低。
推荐修复:
// 先记录原始金额
uint256 originalAmount = (sharesToBurn * totalStaked) / totalShares;
uint256 amountToUser = originalAmount;
if (block.timestamp < user.depositTime + LOCK_PERIOD) {
uint256 fee = (originalAmount * EARLY_WITHDRAWAL_FEE) / FEE_DENOMINATOR;
amountToUser = originalAmount - fee;
}
// totalStaked减去原始金额(包含fee)
totalStaked -= originalAmount;
totalShares -= sharesToBurn;
stakingToken.transfer(msg.sender, amountToUser);
M-01: ERC20 transfer返回值未检查
严重性: Medium 文件: MomoVault.sol 行号: L92, L110, L113, L138, L157, L168
描述:
合约中所有的 stakingToken.transfer() 和 stakingToken.transferFrom() 调用都没有检查返回值。ERC20标准规定 transfer 返回 bool,某些Token(如USDT)在转账失败时不revert而是返回 false。
// 当前实现——未检查返回值
stakingToken.transferFrom(msg.sender, address(this), amount); // L92
stakingToken.transfer(msg.sender, pending); // L110
stakingToken.transfer(msg.sender, amount); // L113
影响:
- 如果transfer失败但没有revert,状态已经更新但资金没有实际转移
- 用户的shares被销毁但Token没有收到
- 或者用户存入0 Token但获得了shares
推荐修复: 使用OpenZeppelin的SafeERC20:
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
// 所有transfer改为safeTransfer
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
stakingToken.safeTransfer(msg.sender, pending);
M-02: deposit中rewardDebt覆盖导致已有奖励丢失
严重性: Medium 文件: MomoVault.sol 行号: L94-L95
描述:
当用户多次deposit时,rewardDebt 被直接覆盖:
user.shares += shares;
user.rewardDebt = (user.shares * rewardPerShare) / 1e18; // 覆盖!
如果用户在第一次deposit后累积了一些未领取的奖励,第二次deposit时 rewardDebt 会被重新计算为 user.shares * rewardPerShare / 1e18。但此时 user.shares 已经包含了新增的shares,而 rewardPerShare 没有包含之前未领取的奖励部分。
结果:用户之前累积的未领取奖励被"清零"了。
推荐修复:
function deposit(uint256 amount) external {
updateRewards();
UserInfo storage user = userInfo[msg.sender];
// 先领取已有奖励
if (user.shares > 0) {
uint256 pending = (user.shares * rewardPerShare) / 1e18 - user.rewardDebt;
if (pending > 0) {
stakingToken.safeTransfer(msg.sender, pending);
emit RewardClaimed(msg.sender, pending);
}
}
// 计算新份额
uint256 shares = /* ... */;
// 更新用户信息
user.shares += shares;
user.rewardDebt = (user.shares * rewardPerShare) / 1e18;
// ...
}
M-03: depositTime每次deposit被覆盖,锁定期可被重置
严重性: Medium 文件: MomoVault.sol 行号: L96
描述:
user.depositTime = block.timestamp; // 每次deposit都重置
当用户多次deposit时,depositTime 被更新为最新的deposit时间。这意味着如果用户在第6天(接近7天解锁期结束时)追加存款,锁定期会被重置,导致用户需要再等7天才能免费取出所有资产(包括之前已经锁定6天的部分)。
这对用户不公平,也可能导致用户不敢追加存款。
推荐修复: 方案A:使用加权平均时间
user.depositTime = (user.shares * user.depositTime + shares * block.timestamp)
/ (user.shares + shares);
方案B:分别追踪每笔存款
struct DepositInfo {
uint256 shares;
uint256 depositTime;
}
mapping(address => DepositInfo[]) public deposits;
L-01: emergencyWithdraw未调用updateRewards
严重性: Low 文件: MomoVault.sol 行号: L158-L170
描述:
emergencyWithdraw 没有调用 updateRewards()。虽然emergency withdraw的设计意图是"放弃奖励快速取出",但不调用 updateRewards 意味着:
rewardPerShare没有更新到最新值- 其他用户在此之后调用任何函数时才会触发更新
- 如果在emergencyWithdraw后紧接着有人withdraw或claimReward,他们的奖励计算会基于过时的
rewardPerShare
推荐修复:
function emergencyWithdraw() external {
updateRewards(); // 添加这行
UserInfo storage user = userInfo[msg.sender];
// ...
}
L-02: 缺少admin转移功能和zero address检查
严重性: Low
描述:
合约没有提供admin转移功能。如果admin私钥丢失,setRewardRate 和 depositRewards 将永远不可调用。constructor中也没有检查 _stakingToken 是否为零地址。
I-01: rewardRate设置缺少上限检查
严重性: Informational
描述:
Admin可以设置任意大的 rewardRate。如果误设为极大值,reward = timeElapsed * rewardRate 可能在短时间内产生巨额奖励承诺,远超Vault中实际可用的Token数量。
DeFi常见漏洞速查清单
Vault / 份额类合约
[ ] 首存者攻击(Inflation Attack)
→ 检查:totalShares == 0时的份额计算逻辑
→ 防御:mint minimum shares到dead address
[ ] 份额计算舍入方向
→ deposit时应向下取整(对用户不利 = 对协议有利)
→ withdraw时应向下取整(对用户不利 = 对协议有利)
→ 如果方向相反,可能被利用
[ ] 直接转Token绕过deposit
→ 检查:是否使用balanceOf追踪还是internal变量
→ 如果用balanceOf,外部转入会膨胀份额价值
[ ] 零份额/零金额检查
→ deposit(0) / withdraw(0) 是否有guard
→ shares = 0时是否仍允许操作
奖励分配类合约
[ ] 精度丢失
→ PRECISION是否足够大(建议至少1e18)
→ reward / totalShares 是否可能截断为0
[ ] 奖励来源
→ 合约中是否有足够Token支付承诺的奖励
→ rewardRate * duration 是否 <= 实际可用奖励
[ ] 追加deposit时的奖励结算
→ 用户追加deposit时,之前的pending reward是否被正确处理
→ rewardDebt是否被正确更新
[ ] 奖励claim的重入
→ claimReward中是否先更新状态再转账(CEI模式)
通用DeFi检查
[ ] 闪电贷攻击向量
→ 合约的核心计算是否依赖instantaneous balance
→ 是否可以在同一交易内deposit+withdraw获利
[ ] ERC20兼容性
→ 是否处理了fee-on-transfer Token
→ 是否处理了rebase Token
→ 是否使用SafeERC20
[ ] 紧急功能
→ 是否有pause/emergency withdraw
→ emergency withdraw是否安全(不影响其他用户)
[ ] 时间锁/锁定期
→ 锁定期在追加操作时的行为
→ 时间戳依赖是否安全
关键要点总结
- DeFi审计核心关注点:份额计算精度、奖励分配正确性、闪电贷交互、资金充足性
- 首存者攻击是Vault的头号威胁:每个Vault合约都必须有防御措施
- Accounting一致性:内部记账(totalStaked)和实际余额(balanceOf)必须保持一致
- SafeERC20是DeFi合约的标配:不检查transfer返回值是常见且危险的遗漏
- 追加操作时的状态处理:用户追加deposit/withdraw时,之前的未结算奖励必须先处理
常见误区
误区1:"我的合约用的是Solidity 0.8.x,不用担心数学问题"
0.8.x防止了溢出,但没有防止精度丢失。(1 * 1e18) / 2e18 = 0 在Solidity中完全合法但可能不符合预期。DeFi中几乎所有的数学漏洞都是精度问题,而非溢出问题。
误区2:"我用了internal变量追踪totalStaked,所以不受直接转入影响"
是的,这比用 balanceOf 更安全。但也要注意:如果有fee-on-transfer的Token,transferFrom(user, vault, amount) 后Vault实际收到的可能小于amount,但 totalStaked += amount 却加了完整的amount。
误区3:"Emergency withdraw不需要太多安全检查"
Emergency withdraw恰恰是最需要安全检查的。在紧急情况下(合约被攻击),emergency withdraw可能是用户唯一的救命通道。它必须:(1) 正确更新所有状态 (2) 不依赖可能被操纵的值 (3) 即使在异常状态下也能工作。
面试关联
Q1: "DeFi Vault合约中最常见的安全问题是什么?"
回答: 最常见的三个问题是:(1) 首存者攻击/Inflation Attack——攻击者通过操纵首次存款的份额比例来窃取后续用户资金;(2) 份额计算精度问题——整数除法舍入可能导致用户获得0 shares;(3) 奖励分配不足——合约承诺的奖励超过实际可用余额,导致最后的用户无法提取。
Q2: "如何防御首存者攻击?"
回答: 主要有三种方式:(1) 像OpenZeppelin ERC4626那样,在首次存款时额外铸造一定数量的shares到dead address,让攻击成本大幅增加;(2) 在份额计算中添加虚拟偏移(virtual offset),使得即使totalShares很小,计算结果也不会被极端操纵;(3) 设置最小首次存款量,确保初始shares足够大。
Q3: "审计一个DeFi协议和审计一个Token合约有什么区别?"
回答: Token合约审计主要关注权限控制和标准合规。DeFi审计额外需要关注:(1) 经济模型正确性——份额、利率、奖励的数学公式是否正确;(2) 跨合约交互——与预言机、其他DeFi协议的组合是否安全;(3) 闪电贷攻击面——合约是否能在同一交易内被利用;(4) 会计一致性——内部记账和实际余额是否匹配。
参考资源
| 资源 | 说明 |
|---|---|
| ERC-4626 Standard | 标准化Vault接口 |
| OpenZeppelin ERC4626 | 参考实现(含首存者攻击防御) |
| First Depositor Attack Explained | MixBytes对Inflation Attack的详解 |
| DeFi Security Best Practices - Nascent | DeFi安全工具包 |
| SWC Registry | 智能合约弱点分类 |
| Solodit | 审计发现数据库,搜索真实案例 |