Summary
checkUpkeep()
function in FeeConversionKeeper.sol
creates marketIds
and assets
arrays with excessive size, so uninitialized elements set to uint128(0)
and address(0)
. This causes a revert
in performUpkeep()
.
Vulnerability Details
marketIds
and assets
arrays length is too big in FeeConversionKeeper.sol:checkUpkeep()
:
function checkUpkeep(bytes calldata ) external view returns (bool upkeepNeeded, bytes memory performData) {
FeeConversionKeeperStorage memory self = _getFeeConversionKeeperStorage();
uint128[] memory liveMarketIds = self.marketMakingEngine.getLiveMarketIds();
bool distributionNeeded;
uint128[] memory marketIds = new uint128[]();
address[] memory assets = new address[]();
uint256 index = 0;
uint128 marketId;
for (uint128 i; i < liveMarketIds.length; i++) {
marketId = liveMarketIds[i];
(address[] memory marketAssets, uint256[] memory feesCollected) =
self.marketMakingEngine.getReceivedMarketFees(marketId);
for (uint128 j; j < marketAssets.length; j++) {
distributionNeeded = checkFeeDistributionNeeded(marketAssets[j], feesCollected[j]);
if (distributionNeeded) {
upkeepNeeded = true;
marketIds[index] = marketId;
assets[index] = marketAssets[j];
index++;
}
}
}
if (upkeepNeeded) {
performData = abi.encode(marketIds, assets);
}
}
FeeConversionKeeper.sol:performUpkeep()
:
function performUpkeep(bytes calldata performData) external override onlyForwarder {
FeeConversionKeeperStorage memory self = _getFeeConversionKeeperStorage();
IMarketMakingEngine marketMakingEngine = self.marketMakingEngine;
(uint128[] memory marketIds, address[] memory assets) = abi.decode(performData, (uint128[], address[]));
for (uint256 i; i < marketIds.length; i++) {
marketMakingEngine.convertAccumulatedFeesToWeth(marketIds[i], assets[i], self.dexSwapStrategyId, "");
}
}
PoC
Create test file in test/integration/external/chainlink/keepers/fee-conversion/performUpkeep/
folder with name performUpkeepRevert.t.sol
:
pragma solidity 0.8.25;
import { Base_Test } from "test/Base.t.sol";
import { FeeConversionKeeper } from "@zaros/external/chainlink/keepers/fee-conversion-keeper/FeeConversionKeeper.sol";
contract FeeConversionKeeper_RevertPerformUpkeep_Integration_Test is Base_Test {
function setUp() public virtual override {
Base_Test.setUp();
changePrank({ msgSender: address(users.owner.account) });
configureMarkets();
changePrank({ msgSender: address(users.naruto.account) });
}
modifier givenCheckUpkeepIsCalled() {
_;
}
function test_FeeConversionKeeperRevert(
uint256 marketId,
uint256 amount,
uint256 minFeeDistributionValueUsd
)
external
givenCheckUpkeepIsCalled
{
PerpMarketCreditConfig memory fuzzPerpMarketCreditConfig = getFuzzPerpMarketCreditConfig(marketId);
minFeeDistributionValueUsd = bound({
x: minFeeDistributionValueUsd,
min: 1,
max: convertUd60x18ToTokenAmount(address(usdc), USDC_DEPOSIT_CAP_X18)
});
amount = bound({
x: amount,
min: minFeeDistributionValueUsd,
max: convertUd60x18ToTokenAmount(address(usdc), USDC_DEPOSIT_CAP_X18)
});
deal({ token: address(usdc), to: address(fuzzPerpMarketCreditConfig.engine), give: amount });
changePrank({ msgSender: users.owner.account });
configureFeeConversionKeeper(1, 1);
FeeConversionKeeper(feeConversionKeeper).setForwarder(users.keepersForwarder.account);
changePrank({ msgSender: address(fuzzPerpMarketCreditConfig.engine) });
marketMakingEngine.receiveMarketFee(fuzzPerpMarketCreditConfig.marketId, address(usdc), amount);
(bool upkeepNeeded, bytes memory performData) = FeeConversionKeeper(feeConversionKeeper).checkUpkeep("");
(uint128[] memory marketIds, address[] memory assets) = abi.decode(performData, (uint128[], address[]));
assertTrue(upkeepNeeded);
assertEq(assets[0], address(usdc));
assertEq(marketIds[0], fuzzPerpMarketCreditConfig.marketId);
for (uint256 i = 1; i < marketIds.length; i++) {
marketIds[i - 1] = marketIds[i];
assets[i - 1] = assets[i];
}
performData = abi.encode(marketIds, assets);
changePrank({ msgSender: users.keepersForwarder.account });
vm.expectRevert();
FeeConversionKeeper(feeConversionKeeper).performUpkeep(performData);
}
}
In cmd run this command:
forge test -vvvvv --mt test_FeeConversionKeeperRevert
Output:
│ │ ├─ [8743] MarketMakingEngine::convertAccumulatedFeesToWeth(0, 0x0000000000000000000000000000000000000000, 1, 0x)
│ │ │ ├─ [5981] FeeDistributionBranch::convertAccumulatedFeesToWeth(0, 0x0000000000000000000000000000000000000000, 1, 0x) [delegatecall]
│ │ │ │ └─ ← [Revert] CollateralDisabled(0x0000000000000000000000000000000000000000)
│ │ │ └─ ← [Revert] CollateralDisabled(0x0000000000000000000000000000000000000000)
│ │ └─ ← [Revert] CollateralDisabled(0x0000000000000000000000000000000000000000)
│ └─ ← [Revert] CollateralDisabled(0x0000000000000000000000000000000000000000)
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.05s (3.01s CPU time)
Impact
It is impossible to convert accumulated fees to WETH through FeeConversionKeeper.performUpkeep()
Tools Used
Manual Review
Recommendations
marketIds
and assets
arrays length should be equal to all markets assets combined (if distributionNeeded = true
for asset).