sherlock-LEND

这是一个关于跨链的借贷,那么漏洞点就是在跨链的处理上

高gas费用阻碍从边界用户退出质押

原因就是使用数组记录数据,并且没有限制,导致gas线性增加,用户支付不了,退出质押
代码漏洞

// 遍历数组确定相关层级变更区间  
for (uint256 i = 0; i < userTierHistory.length; i++) {
if (userTierHistory[i].timestamp <= fromTime) {
currentTier = userTierHistory[i].to;
relevantStartIndex = i;
} else {
break;
}
}

// 计算区间内各层级的利息
for (uint256 i = relevantStartIndex + 1; i < userTierHistory.length; i++) {
if (userTierHistory[i].timestamp >= toTime) break;
// 解析时间段、层级及APY,计算利息
uint256 periodInterest = ((user.balance * apy * (periodEnd - periodStart)) / (SECONDS_IN_YEAR * PRECISION)) / 100;
totalInterest += periodInterest;
}

数组长度越大,循环次数越多,gas 消耗呈线性增长,直至超过用户愿意支付的成本。

跨链操作引发的漏洞,跨链借贷使用同一种抵押物

若未在源链锁定抵押品,协议将面临抵押不足的债务风险。借款人可在跨链借贷完成前提取或重复使用同一抵押品,从而实现跨链超额借贷。

function borrowCrossChain(...) {  
// 仅发送跨链消息,未锁定抵押品
sendLayerZeroMessage(...);
}

攻击者在链 B 获取借贷资金的同时,已提空链 A 抵押品,导致协议遭受全额资金损失。

跨链检查条件错误,源链和目标链匹配错误

计算跨链抵押品生息债务的条件检查错误地要求源链 ID(srcEid)和目标链 ID(destEid)均匹配当前链 ID(currentEid)。然而,对于跨链借贷场景,源链与目标链必然不同,导致borrowedAmount被计算为 0,最终阻断债务偿还流程。

function borrowWithInterest(...) public view returns (uint256) {  
// ...
for (uint256 i = 0; i < collaterals.length; i++) {
// 错误要求源链和目标链均等于当前链
if (collaterals[i].destEid == currentEid && collaterals[i].srcEid == currentEid) {
borrowedAmount += (...);
}
}
// ...
}

跨链借贷的srcEid与destEid必然不同(例如源链为 A,目标链为 B),而currentEid仅为其中一条链(如 B),导致条件永远不成立,borrowedAmount始终为 0。

未扣除同一抵押物,用户可以在多条链上反复重复借贷

发起跨链借贷时,源链仅向目标链发送原始抵押品价值,未扣除用户已存在的借贷金额。这使得用户可利用同一抵押品在多条链上重复借贷,导致系统性抵押不足风险。

大概的攻击路径:

  • 抵押品存入与首次借贷:
    • 用户在链 A 存入 1000 USDC(抵押率 80%,对应 800 美元借贷额度)。
    • 在链 A 借贷 600 USDT,剩余可用额度 200 美元。
  • 跨链借贷发起:
    • 用户从链 A 调用borrowCrossChain向链 B 借贷 700 USDT。
    • 源链计算抵押品为 800 美元(未扣除已借贷的 600 美元),并将该值发送至链 B。
    • 链 B 接收到 800 美元抵押品值,允许借贷 700 USDT(因验证条件为抵押品 ≥ 借贷金额)。
  • 结果:
    • 用户累计借贷 1300 美元(600+700),但实际可用抵押品额度仅 800 美元(利用率 162.5%)。

领取奖励后,要更新余额,不然会导致重复领取

grantLendInternal()函数虽成功转移代币,但返回的剩余金额(成功时为 0)未被使用。这意味着lendStorage.lendAccrued[holders[j]]仍保留原值,用户可再次申领。

for (uint256 j = 0; j < holders.length;) {  
uint256 accrued = lendStorage.lendAccrued(holders[j]);
if (accrued > 0) {
grantLendInternal(holders[j], accrued); // 转移代币但未重置余额
}
unchecked {
++j;
}
}

使用的历史利息错误

exchangeRateStored仅反映历史汇率,未通过accrueInterest()更新应计利息,而exchangeRateCurrent才是包含实时利息的正确汇率。

uint256 exchangeRateBefore = LTokenInterface(_lToken).exchangeRateStored();  

错误的清算

清算权限判断逻辑错误地将全局借款金额与市场特定的借款指数比值相乘:

uint256 borrowedAmount =  
(borrowed * uint256(LTokenInterface(lTokenBorrowed).borrowIndex())) / borrowBalance.borrowIndex;

require(borrowedAmount > collateral, "Insufficient shortfall");

然而 getHypotheticalAccountLiquidityCollateral 函数返回的 borrowed 已经是用户在所有市场中的全局债务值,并已经包含利息。因此不应再额外缩放 borrow index,否则就会导致与抵押品价值的不一致比较。

借款时使用了错误的借款指数

对于跨链借款,其利息应使用目标链(目的链)借款市场的 borrowIndex 进行计算。但当前实现中,错误地使用了源链的 borrowIndex。由于不同链的市场启动时间、借款活跃度和利率差异,这些指数可能会相差很大。

错误更新借款状态

在跨链中,不应该只考虑在跨链的操作,如果在本链还有借贷物,就会被一起抵消、

攻击路径:

  • 攻击者在链 A 上调用 CoreRouter::supply 供应一点点资产作为抵押;

  • 调用 CrossChainRouter::borrowCrossChain 发起一次小额的跨链借款(目标链是链 B);

  • 跨链借款成功后,攻击者前往链 B;

  • 在链 B 上再次调用 CoreRouter::supply 和 CoreRouter::borrow 借更多资产;

  • 调用 CrossChainRouter::repayCrossChainBorrow,还清那笔小额跨链借款;

  • 此时协议会把攻击者在链 B 的借款记录清零,尽管他实际上还欠一堆钱;

  • 攻击者调用 CoreRouter::redeem,把他在链 B 所有的抵押品都提走;

  • 协议被洗劫,所有流动性被清空。

跨链借贷,在本链的抵押物没有被锁定,导致本链可以使用,跨链也可以使用

在 CrossChainRouter.sol 的 borrowCrossChain() 函数中:

lendStorage.addUserSuppliedAsset(msg.sender, _lToken);

这句只是记录了用户的抵押品信息,但没有锁定这部分抵押资产。

然后协议通过 _send() 向目标链发送跨链消息,但 在消息尚未被处理前,用户仍然可以在源链操作这些抵押资产。

在 CoreRouter.sol 中的 redeem() 函数中:

(uint256 borrowed, uint256 collateral) = 
lendStorage.getHypotheticalAccountLiquidityCollateral(msg.sender, LToken(_lToken), _amount, 0);
require(collateral >= borrowed, "Insufficient liquidity");

这里的流动性检查只检查了当前链上的借款情况,并未考虑正在跨链进行中的借款请求。