Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Valid

FeeConversionKeeper `checkUpkeep()` returns erroneous values for `performData`

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:

// decode performData
(uint128[] memory marketIds, address[] memory assets) = abi.decode(performData, (uint128[], address[]));
// convert accumulated fees to weth for decoded markets and assets
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
{
// loads the collateral data storage pointer, collateral must be enabled
Collateral.Data storage collateral = Collateral.load(asset);
collateral.verifyIsEnabled(); //@audit revert here

Impact

Protocol Functionality affected:

  • Automation will always revert.

  • Fees accumulated in assets such as USDC cannot be converted to WETH, disrupting rewards distribution.

POC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import { Errors } from "@zaros/utils/Errors.sol";
// Zaros dependencies
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);
// call performUpkeep
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(){
// ...
// omitting code above for brevity
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);
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

FeeConversionKeeper performUpkeep iterates over entire oversized arrays instead of just populated entries, causing reverts on address(0) slots

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.