cantina-size Credit

使用owner()函数,该合约已经设置了owner为零

漏洞代码

    function reinitialize() external onlyOwner reinitializer(1_7_0) {
// grant `AccessControlUpgradeable` roles to the `Ownable2StepUpgradeable` owner
_grantRole(DEFAULT_ADMIN_ROLE, owner());
_grantRole(PAUSER_ROLE, owner());
_grantRole(KEEPER_ROLE, owner());
_grantRole(BORROW_RATE_UPDATER_ROLE, owner());
// transfer `Ownable2StepUpgradeable` ownership to the zero address to keep the state consistent
// in a future upgrade, we can simply remove `Ownable2StepUpgradeable` from the implementation
-> _transferOwnership(address(0)); //设置owner为零
// can only be called once
}

后续在创造Market市场,使用owner()函数,直接使用的该合约的地址,为零

    function createMarket(
InitializeFeeConfigParams calldata feeConfigParams,
InitializeRiskConfigParams calldata riskConfigParams,
InitializeOracleParams calldata oracleParams,
InitializeDataParams calldata dataParams
) external onlyRole(DEFAULT_ADMIN_ROLE) returns (ISize market) {
market = MarketFactoryLibrary.createMarket(
-> sizeImplementation, **owner()**, feeConfigParams, riskConfigParams, oracleParams, dataParams
);
_addMarket(market);
}

后面会对零地址的一个讨论,所以该创建市场始终不会成功

function validateOwner(address owner) internal pure {
if (owner == address(0)) {
revert Errors.NULL_ADDRESS();
}
}

具体修改就是不使用owner(),使用管理者

+         address admin = msg.sender;
market = MarketFactoryLibrary.createMarket(
- sizeImplementation, owner(), feeConfigParams, riskConfigParams, oracleParams, dataParams
+ sizeImplementation, admin, feeConfigParams, riskConfigParams, oracleParams, dataParams
);

gas消耗,调用函数,重复使用修饰

在 compensate() 函数中,使用了两次 shouldNotEndUpUnderwater 修饰符:

一次是在 compensateOnBehalfOf() 函数中应用,传入了externalParams.onBehalfOf 作为参数。
另一次是在 compensate() 函数内部,直接应用于 msg.sender
关键点是,``msg.sender compensate()函数中等同于externalParams.onBehalfOf,因为 compensate() 调用了 compensateOnBehalfOf(externalParams.onBehalfOf)`。

由于两者(msg.sender 和 externalParams.onBehalfOf)是相同的,实际上第二次使用 shouldNotEndUpUnderwater(msg.sender) 是多余的。这样会导致在计算是否用户资金低于清算线(underwater)时,执行了重复的检查,从而增加了不必要的 gas 消耗

通过前置交易还清债务,导致清算失败

借款人借钱并借款位置被清算:

假设借款人借了钱,市场发生波动,导致他们的抵押物(例如 WETH 或 USDC)价值下降,贷款比例超过了安全阈值(例如市场的风险配置)。
这时,清算者会尝试清算借款人的债务,回收他们的抵押物。
借款人如何规避清算:

借款人调用 compensate() 函数。这时,借款人偿还了他们的所有债务,并创建了一个新的债务位置。新位置可能是一个具有不同期限或条件的贷款。
这时,借款人“买时间”,原本的贷款债务被视为已经偿还,因为系统认为它已经“被清偿”了。
清算失败:

清算者尝试清算借款人原本的债务,但发现这个债务已经被“偿还”了,因为在 compensate() 中,借款人将原本的债务状态更新为“已偿还”,并创建了一个新的债务位置。
因此,清算者无法清算这个债务位置,导致清算操作失败。
前置交易的关键点:

前置交易(front-running):借款人通过调用compensate()来提前偿还债务并创建新位置。这相当于“转移”了债务的负担,清算者就无法对原债务进行清算。
拖延清算时间:借款人通过反复创建新的债务位置,能够持续拖延清算,直到贷款的期限结束。
示例
假设借款人A(例如 Bob)借了 100 WETH,贷款期限为 7 天。如果市场不稳定,贷款在第3天就会达到清算阈值。清算者打算清算 Bob 的债务,但在清算前,Bob 调用了 compensate() 函数,偿还了他当前的债务,并创建了一个新的债务位置,这个新的债务位置可能会有不同的条件(例如不同的期限)。

当清算者再次尝试清算时,系统认为 Bob 的债务已经偿还(因为 compensate() 更新了债务状态),所以清算操作失败。Bob 通过这种方式“买了时间”,避免了清算。

影响
借款人:借款人通过这种方式避免了被清算,并且可以继续延缓清算,直到债务过期。
清算者:清算者无法收回债务,因为借款人通过 compensate() 创建了新的债务位置,导致原债务位置被视为已偿还。

升级合约时,没有显式的调用初始化函数

MulticallUpgradeableAccessControlUpgradeable 合约都有各自的初始化函数(例如 __Multicall_init 和 __AccessControl_init)。
这些初始化函数通常在合约部署时被调用,用于设置初始状态和配置。
然而,在合约升级(使用 reinitialize)的过程中,除非显式调用初始化函数,否则这些合约的初始化函数不会自动被调用。
问题的关键
当升级 SizeFactory 合约时,reinitialize 函数会被调用,但它没有显式调用依赖合约(MulticallUpgradeable 和 AccessControlUpgradeable)的初始化函数。
结果是,在升级之后,MulticallUpgradeableAccessControlUpgradeable 合约不会被正确初始化。虽然你提到目前这个问题没有风险,因为它们的初始化函数不执行任何实际的逻辑,但如果将来这些合约的初始化函数发生变化或有逻辑执行,那么这可能会导致问题。

升级后的合约,一些函数缺少修饰,导致恶意用户调用

在升级后,createPriceFeedcreateBorrowATokenV1_5 函数缺少授权修饰符,这意味着任何用户都可以调用这些函数。这是一个问题,因为它允许恶意用户将恶意的价格喂价或借贷代币加入白名单,而这些恶意合约可能会被 Size 市场使用。

例如,问题可能会引发以下情况:
恶意价格喂价:市场可能会使用一个恶意的 UniswapV3 池合约,该合约返回不准确的价格,或者返回偏袒某些地址的价格。恶意用户可以利用这一点,将实际的抵押率调整为高于真实值,从而防止可能的清算。

恶意借贷代币:恶意借贷代币可能会使用一个恶意的变量池,如果某些用户尝试调用 Size.withdraw,则在 NonTransferrableScaledTokenV1_5.withdraw 中的 variablePool.withdraw 会因错误而被拒绝,从而阻止这些用户提款。

variablePool.withdraw(address(underlyingToken), amount, to);
过多的价格喂价/借贷代币:恶意用户可以创建大量的价格喂价或借贷代币,这将导致 getPriceFeedsgetBorrowATokensV1_5 调用失败,因为这两个函数会遍历所有实例,可能会超出区块的 gas 限制。此外,恶意用户还可以创建具有过长描述的价格喂价或借贷代币,导致调用 getBorrowATokenV1_5DescriptionsgetPriceFeedDescriptions 时因为 gas 不足而回滚,或者需要大量 gas,最终导致资金损失。