Summary
The FeeConversionKeeper.sol contract is a Chainlink Automation Contract that is Logic Based Triggered. The function checkUpkeep() returns (bool upkeepNeeded, bytes memory performData).
If upkeepNeeded is true, the exact returned value for performData will be used in the function performUpkeep(). Due to erroneous values for performData returned from checkUpKeep(), performUpkeep() will always revert.
Vulnerability Details
In the checkUpkeep() function, the performData is the prepared as such
performData = abi.encode(marketIds, assets);
However, marketIds and assets are first initialized as such:
uint128[] memory marketIds = new uint128[](liveMarketIds.length * 10);
address[] memory assets = new address[](liveMarketIds.length * 10);
Let's assume that liveMarketIds.length is 10, each array will have a size of 100, but only a small subset of this array may contain valid data.
The remaining slots are populated with invalid defaults of uint128(0) and address(0).
These invalid entries are encoded into performData and passed to performUpkeep(), which iterates over all slots:
(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, "");
}
When the argument asset is address(0) , and is then passed into convertAccumulatedFeesToWeth(), collateral.verifyIsEnabled(); will cause the transaction to revert.
function convertAccumulatedFeesToWeth(
uint128 marketId,
address asset,
uint128 dexSwapStrategyId,
bytes calldata path
)
external
onlyRegisteredSystemKeepers
{
Collateral.Data storage collateral = Collateral.load(asset);
collateral.verifyIsEnabled();
Impact
Protocol Functionality affected:
Automation will always revert.
Fees accumulated in assets such as USDC cannot be converted to WETH, disrupting rewards distribution.
POC
pragma solidity 0.8.25;
import { Errors } from "@zaros/utils/Errors.sol";
import { Base_Test } from "test/Base.t.sol";
import { FeeConversionKeeper } from "@zaros/external/chainlink/keepers/fee-conversion-keeper/FeeConversionKeeper.sol";
contract FeeConversionKeeperPOC is Base_Test {
function setUp() public virtual override {
Base_Test.setUp();
changePrank({ msgSender: address(users.owner.account) });
createVaults(marketMakingEngine, INITIAL_VAULT_ID, FINAL_VAULT_ID, true, address(perpsEngine));
configureMarkets();
changePrank({ msgSender: address(users.naruto.account) });
}
modifier givenCheckUpkeepIsCalled() {
_;
}
function test_FeeConversionKeeperPerformData(
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);
changePrank({ msgSender: address(fuzzPerpMarketCreditConfig.engine) });
marketMakingEngine.receiveMarketFee(fuzzPerpMarketCreditConfig.marketId, address(usdc), amount);
(bool upkeepNeeded, bytes memory performData) = FeeConversionKeeper(feeConversionKeeper).checkUpkeep("");
assertTrue(upkeepNeeded);
(uint128[] memory marketIds, address[] memory assets) = abi.decode(performData, (uint128[], address[]));
assertEq(assets[0], address(usdc));
assertEq(marketIds[0], fuzzPerpMarketCreditConfig.marketId);
changePrank({ msgSender: users.owner.account });
FeeConversionKeeper(feeConversionKeeper).setForwarder(users.keepersForwarder.account);
changePrank({msgSender: users.keepersForwarder.account});
vm.expectRevert(abi.encodeWithSelector(Errors.CollateralDisabled.selector, address(0)));
FeeConversionKeeper(feeConversionKeeper).performUpkeep(performData);
}
}
Tools Used
Manual Review
Recommendations
Change the bottom part of the code to be
function checkUpkeep(){
if (upkeepNeeded) {
uint128[] memory correctMarketIds = new uint128[]();
address[] memory correctAssets = new address[]();
for (uint i; i< index; i++){
correctMarketIds[i] = marketIds[i];
correctAssets[i] = assets[i];
}
performData = abi.encode(correctMarketIds, correctAssets);
}
}