LpdFi 协议 $693K 闪电贷价格操纵攻击 深度源码剖析与修复方案
2026年8月2日,BNB Chain 上的 LpdFi 协议遭遇攻击,损失约 693,529 USDC。攻击者在偿还所有临时流动性后,净获利约 573,000 美元。这次事件的规模固然引人注目,但其背后的技术逻辑更加发人深省, 它并非复杂的重入攻击或未知的 0-day 漏洞,而是一次对预言机依赖、利息结算机制和协议全局一致性缺失的经典教科书式利用。
本文将带你逐步拆解攻击的每一个环节,深入分析存在漏洞的代码片段,并提供经过验证的修复方案,希望能帮助开发者们避开同样的坑。
攻击流程:分步拆解(Step-by-Step)
步骤一:铺垫交易 操纵现货价格
攻击者首先部署了一个执行合约,并注入 116,495 USDC 作为种子资金。随后,他们通过闪电贷从 Lista DAO 的 Moolah、Venus、Aave、Uniswap V4 等多个流动性池中临时借入了约 4400 万 USDC。
在同一笔交易中,攻击者将这 4400 万 USDC 全部投入 PancakeSwap V2 的 LPD/USDC 交易对,换回了 4,788,064.49 枚 LPD 代币。这一操作直接导致 LPD 的现货价格从 0.126899 USDC/LPD 暴力拉升至 655.197923 USDC/LPD 暴涨了 5,163 倍。
关键点:攻击者并不需要长期维持这个高价,只需要在这一笔交易的执行瞬间扭曲协议的价值计算函数即可。
步骤二:创建虚高仓位(Inflated Position)
价格被拉高后,攻击者调用 LpdFi.buy(uAmount),传入了 140,324,732e18 作为名义 USDC 本金。
这里存在致命的设计缺陷:Lpd.price() 直接从 PancakeSwap 交易对的当前储备量中取值,没有采用时间加权平均价格(TWAP),也没有任何外部喂价或偏离度检查。
- 按操纵后的天价计算,协议认为仅需 214,171.52 枚 LPD 就能抵押出 1.4 亿美元 的仓位。
- 实际上,攻击者存入的 LPD 价值远低于名义上的 1.4 亿美元。
协议内部记录了一个虚假的 uAmount(1.4 亿 USDC)和 interestTop(约 7016 万 USDC)。这些数字仅仅存在于协议的账本中,并没有任何真实的 USDC 资产作为支撑。做完这一切后,攻击者立刻将大部分 LPD 换回 USDC,并偿还了所有的闪电贷,让 LPD/USDC 池子恢复“正常”状态。
步骤三:利用时间边界 一秒钟套取全天利息
铺垫交易的时间戳是 15:59:59 UTC。仅仅一秒之后,即 16:00:00 UTC,攻击者提交了领取利息的交易。
LpdFi 的利息累加机制依赖于离散的 issue 索引, 每个索引代表一个完整的日常计息周期。当时间跨过边界,issue 索引从 18 跳转到 19 时,getOrder() 函数会计算 uAmount 乘以日利率(0.5%)再乘以经过的 issue 数量。
一个只存在了 1 秒钟的仓位,因为跨越了 issue 边界,竟然获得了完整的 0.5% 日收益率。基于虚假的 1.4 亿美元本金,计算出的利息高达 701,623.66 USDC。
步骤四:抽干协议流动性(Drain)
为了提取这 70 万利息,攻击者调用 claimInterest()。但在提取之前,他们故技重施,又闪电贷借了 730,607.76 USDC,将其中 3,440.99 USDC 转入 LPD/USDC 交易对,再次短暂地同步并扭曲了池子的储备量。
为什么这么做?因为 claimInterest() 是通过调用 removeLp() 来支付利息的,而 removeLp() 计算需要燃烧多少 LP 代币时,依赖的是交易对实时的 USDC 储备量。通过临门一脚的储备操纵,攻击者确保了协议会燃烧最大数量的 LP 头寸。
最终,协议燃烧了 1,678,049.36 枚 LP 代币,释放出 700,535.14 USDC 和 4,059,427.51 LPD。其中 693,529.79 USDC 直接进入了攻击者的辅助合约,剩下的 7,005.35 USDC 则流向了协议配置的费用地址。
最致命的细节:两次 removeLp() 调用均使用了零最小值输出参数(minUSDC = 0, minLPD = 0)。没有任何滑点保护,没有任何合理性检查,协议眼睁睁地看着自己的全部仓位被掏空。
源码层面:到底错在哪了?(Vulnerabilities Analysis)
漏洞一:现货价格预言机(Spot-Price Oracle)
漏洞代码核心在于 Lpd.price() 函数:
function price() public view returns (uint256) {
// 直接从 PancakeSwap 读取瞬时储备量
// 无 TWAP,无抗操纵能力
(uint112 reserve0, uint112 reserve1, ) = IPancakePair(pair).getReserves();
return reserve1 * 1e18 / reserve0; // LPD/USDC 现货价格
}
该函数直接读取交易对的瞬时储备并计算现货价格。只要攻击者拥有足够的临时流动性(本例为 4400 万 USDC),就能在单个区块内轻松扭曲价格。没有 TWAP,没有流动性深度阈值,没有偏离度检查。这是 DeFi 领域最常见的预言机漏洞。
漏洞二:基于离散索引的计息逻辑(Discrete Interest Accrual)
利息计算机制存在严重的时间边界漏洞:
function getOrder(uint256 index) public view returns (Order memory) {
Order storage order = orders[index];
uint256 elapsedIssues = issue – order.lastIssue;
uint256 interest = order.uAmount * dailyRate * elapsedIssues / 1e18;
// …
}
利息基于当前 issue 索引与订单创建时 lastIssue 索引的差值计算。每个 issue 代表一整天的周期。如果你在 issue=18 创建订单,在 issue=19 领取,无论中间实际经历了一秒钟还是 24 小时,你都会获得一整天的利息。这种设计让协议在日切边界变得极其脆弱。
漏洞三:零滑点保护(Zero Slippage Protection)
removeLp() 的调用方式极其危险:
function claimInterest(uint256 index) external {
// … 计算利息数额 …
removeLp(interestAmount, 0, 0); // amountOutMin = 0, amountOutMinLPD = 0
}
function removeLp(uint256 usdcAmount, uint256 minUSDC, uint256 minLPD) internal {
// 销毁 LP 并提取,最小输出为 0
IPancakeRouter(router).removeLiquidity(
address(LPD),
address(USDC),
lpAmount,
minLPD, // 0
minUSDC, // 0
address(this),
block.timestamp
);
}
minUSDC 和 minLPD 均被硬编码为 0,意味着协议愿意接受任何数额的提取,哪怕池子储备被严重操纵到只剩下一点点 USDC。这等于放弃了所有滑点保护。
漏洞四:状态验证不一致(Inconsistent State Validation)
最根本的问题是,协议用同一个可操纵的状态源(PancakeSwap 的储备量)来执行两个关键操作:
协议从未验证提取的利息是否由真实的资产所支撑。它将存款估值、利息累积和赎回视为独立的操作,而不是在仓位的整个生命周期中持续验证偿付能力。
修复方案:如何避免这类攻击?(The Fixes)
修复一:使用抗操纵的预言机(TWAP / Chainlink)
弃用现货价格,引入时间加权平均价格(TWAP):
import "@uniswap/v2-periphery/contracts/libraries/UniswapV2OracleLibrary.sol";
contract LpdFi {
using UniswapV2OracleLibrary for IPancakePair;
IPancakePair public pair;
uint256 public constant TWAP_WINDOW = 30 minutes;
function price() public view returns (uint256) {
// 采用 30 分钟的时间加权平均价格
(uint256 price0, uint256 price1) = UniswapV2OracleLibrary
.consult(pair, TWAP_WINDOW);
return price1 * 1e18 / price0;
}
}
采用 30 分钟 TWAP 后,攻击者若想操纵价格,必须将巨额闪电贷维持 30 分钟以上,这将产生巨额资金费用,使攻击在经济上变得不可行。更进一步的方案是使用 Chainlink 喂价或多数据源聚合。
修复二:基于真实时间的连续计息
将离散索引计息改为真实时间戳连续计息:
struct Order {
uint256 uAmount;
uint256 lastUpdate;
uint256 accruedInterest;
// … 其他字段
}
function updateOrder(uint256 index) internal {
Order storage order = orders[index];
uint256 timeElapsed = block.timestamp – order.lastUpdate;
uint256 interest = order.uAmount * dailyRate * timeElapsed / (1 days * 1e18);
order.accruedInterest += interest;
order.lastUpdate = block.timestamp;
}
利息与实际经过的时间(秒数)严格成正比,彻底消除了一秒跨越边界获取全天利息的套利空间。在这种机制下,攻击者一秒钟只能赚取约 1,624 美元,而不是 701,623 美元。
修复三:添加滑点保护与合理性校验
永远不要在生产环境中使用零滑点参数:
function claimInterest(uint256 index) external {
Order memory order = getOrder(index);
uint256 interest = order.accruedInterest;
// 基于可信预言机计算预期应得的 USDC 数量
uint256 expectedUSDC = getFairUSDCAmount(interest);
uint256 minUSDC = expectedUSDC * 95 / 100; // 5% 滑点容忍度
// 同时校验协议实际拥有足够流动性
require(
getProtocolUSDCBalance() >= interest,
"Insufficient protocol liquidity"
);
removeLp(interest, minUSDC, 0);
}
即使资金池被临时操纵,协议也不会接受严重偏离公允价值的提取数额。结合 TWAP 预言机,这形成了多层防护。
修复四:全生命周期偿付能力验证
在每一个退出点验证负债是否有足额资产支撑:
function claimInterest(uint256 index) external {
Order memory order = getOrder(index);
// 验证订单的本金是否仍然由真实的 LPD 支撑
uint256 currentLPDValue = getFairLPDValue(order.lpdAmount);
require(
currentLPDValue >= order.uAmount,
"Position undercollateralized"
);
// 验证通过后才允许领取利息
// …
}
价格操纵被撤销后,攻击者虚高的 1.4 亿美元仓位会立即触发该检查(currentLPDValue 远小于 uAmount),从而阻止任何虚假利息的提取。
修复五:引入断路器(Circuit Breaker)与每日限额
contract LpdFi {
bool public paused;
uint256 public maxDailyInterestPayout;
uint256 public dailyPayoutTotal;
uint256 public lastPayoutReset;
modifier whenNotPaused() {
require(!paused, "Protocol paused");
_;
}
function claimInterest(uint256 index) external whenNotPaused {
if (block.timestamp >= lastPayoutReset + 1 days) {
dailyPayoutTotal = 0;
lastPayoutReset = block.timestamp;
}
uint256 interest = calculateInterest(index);
require(
dailyPayoutTotal + interest <= maxDailyInterestPayout,
"Daily payout limit exceeded"
);
dailyPayoutTotal += interest;
// … 继续执行提取
}
}
即使所有前面的防护都失效了,每日赔付上限也能将单日最大损失限制在可控范围内。在本案例中,70 万美元的单笔赔付会直接触发断路器,阻止资金外流。
给开发者的核心启示录(Key Takeaways)
永远不要盲目信任现货价格。 如果你的协议依赖 DEX 价格来做估值(而非仅仅用于执行 swap),务必使用 TWAP 或专业的预言机解决方案。闪电贷让单区块价格操纵变得既廉价又简单。
时间是连续的,不是离散的。 基于区块号或人工索引(issue)来计算利息,必然会产生边界条件,而攻击者最擅长挖这些边界。请基于真实的时间戳(block.timestamp)进行连续累加。
零绝不是有效的滑点参数。 amountOutMin = 0 不是一个方便的默认值,而是一个随时可能引爆的定时炸弹。始终基于公允市场价格设定合理的滑点容忍下限。
在每一个退出点验证偿付能力。 不要假设入场时验证过的仓位在出场时依然健康。市场会变,协议的内部状态也会因为各种交互而改变。
将存款估值、利息结算和赎回视为一个不可分割的整体系统。 如果你在存款和赎回时使用了同一个预言机,你就可能创造出一个循环依赖关系,而这个关系正是攻击者最理想的突破口。
结语:安全是一场持续进化的攻防战
LpdFi 的这次攻击并不高级, 它是可预测的。现货价格操纵、日切利息套利、零滑点提取,这些攻击手法在历年的安全审计报告和漏洞复盘文章中已经被反复提及。问题的根源不在于开发者粗心,而在于 DeFi 协议是一个复杂的系统工程,看似独立的模块在极端条件下会产生危险的耦合效应。
单独加一个 TWAP 预言机,如果计息逻辑仍然存在边界漏洞,攻击者依然可能找到新的路径;单独加滑点保护,如果预言机源本身可被操纵,保护就成了摆设。真正的安全在于纵深防御(Defense in Depth), 独立验证、持续偿付检查、多层限流措施,这些不是可选项,而是任何管理用户资金的协议必须达到的最低标准。
欢迎一起交流讨论
感谢你读到这里。希望这篇深度剖析能帮你厘清闪电贷价格操纵攻击的底层逻辑,也希望对你在设计和审计 DeFi 协议时有所帮助。
如果你正在开发区块链项目,或者在审计智能合约时遇到了棘手的逻辑隐患,非常欢迎在 CSDN 评论区留言,或者直接私信我。我们可以聊聊这次 LpdFi 事件的更多细节,也可以一起 brainstorm 你项目中的特定防御策略。
安全这条路道阻且长,但每一次深入的技术复盘都能让我们走得更稳。期待和更多志同道合的开发者交流,共同为链上生态的安全添砖加瓦。
网硕互联帮助中心




评论前必须登录!
注册