Part 2

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

The protocol is insolvent

Summary

When the protocol compute the value of fees, it overestimete it and then make the protocol insolvent.

Vulnerability Details

In order to have fees the keeper must call the function convertAccumulatedFeesToWeth that will swap the fees to WETH and then handler the distribution between the stakers and the fee receipients.

A problem occur when _handleWethRewardDistribution is called this function distribute the amount out of the swap between fee receipients.

function _handleWethRewardDistribution(
Market.Data storage market,
address assetOut,
UD60x18 receivedWethX18
)
internal
{
// cache the total fee recipients shares as UD60x18
UD60x18 feeRecipientsSharesX18 = ud60x18(MarketMakingEngineConfiguration.load().totalFeeRecipientsShares);
// calculate the weth rewards for protocol and vaults
UD60x18 receivedProtocolWethRewardX18 = receivedWethX18.mul(feeRecipientsSharesX18);
UD60x18 receivedVaultsWethRewardX18 =
receivedWethX18.mul(ud60x18(Constants.MAX_SHARES).sub(feeRecipientsSharesX18));
// calculate leftover reward
UD60x18 leftover = receivedWethX18.sub(receivedProtocolWethRewardX18).sub(receivedVaultsWethRewardX18);
// add leftover reward to vault reward
receivedVaultsWethRewardX18 = receivedVaultsWethRewardX18.add(leftover);
// adds the weth received for protocol and vaults rewards using the assets previously paid by the engine
// as fees, and remove its balance from the market's `receivedMarketFees` map
market.receiveWethReward(assetOut, receivedProtocolWethRewardX18, receivedVaultsWethRewardX18);
// recalculate markes' vaults credit delegations after receiving fees to push reward distribution
Vault.recalculateVaultsCreditCapacity(market.getConnectedVaultsIds());
}

The protocol will overestimate the fees because of rounding errors. We can demonstrated with a simple POC.

You can run this POC by copy pasting this code in the performUpkeep.t.sol file in the test/integration/fee-conversion folder. You will have also to import the IERC20 interface in order to approve the token transfers and the console.

import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
import {console2} from "forge-std/Test.sol";

You should have this output we clearly see after the fee conversion the protocol is insolvent :

This is the earned fees after swap fees 409082407123499999
This is the balance after swap fees : 215306530065000000
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 31.24ms (9.25ms CPU time)
Ran 1 test suite in 157.71ms (31.24ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/integration/external/chainlink/keepers/fee-conversion/performUpkeep/performUpkeep.t.sol:FeeConversionKeeper_PerformUpkeep_Integration_Test
[FAIL: ERC20InsufficientBalance(0xe98A0C3C4dE1ea937C993928a5AED70f9ba593ba, 1, 193775877058500000 [1.937e17])] test_insufficientBalance() (gas: 8216316)
uint128[] feeConversionMarketIds;
address[] feeConversionAssets;
function test_insufficientBalance() public {
address bob= 0x0000000000000000000000000000000000010000;
address keeper = users.keepersForwarder.account;
VaultConfig memory fuzzVaultConfig1 = getFuzzVaultConfig(8);
VaultConfig memory fuzzVaultConfig2 = getFuzzVaultConfig(14);
usdc.mint(bob, 100_000e6);
vm.stopPrank();
vm.startPrank(bob);
usdc.approve(address(marketMakingEngine), 100_000e6);
IERC20(fuzzVaultConfig1.indexToken).approve(address(marketMakingEngine), type(uint).max);
IERC20(fuzzVaultConfig2.indexToken).approve(address(marketMakingEngine), type(uint).max);
wEth.mint(bob, 100_000e18);
wEth.approve(address(marketMakingEngine), 100_000e18);
marketMakingEngine.deposit(8, 65046691939 , 27447489388 , "", false);
marketMakingEngine.deposit(14, 1624467398489052174 , 0 , "", false);
PerpMarketCreditConfig memory currentPerpMarketCreditConfig1 = perpMarketsCreditConfig[uint256(3)];
usdc.mint(currentPerpMarketCreditConfig1.engine,434962687);
vm.stopPrank();
vm.startPrank(currentPerpMarketCreditConfig1.engine);
usdc.approve(address(marketMakingEngine), 434962687);
marketMakingEngine.receiveMarketFee(3, address(usdc), 434962687);
vm.stopPrank();
vm.prank(bob);
marketMakingEngine.stake(8, 100000 );
vm.prank(bob);
marketMakingEngine.stake(14, 100146 );
vm.startPrank( users.owner.account);
configureFeeConversionKeeper(1, uint128(1));
FeeConversionKeeper(feeConversionKeeper).setForwarder(keeper);
vm.stopPrank();
(bool upkeepNeeded, bytes memory performData) = FeeConversionKeeper(feeConversionKeeper).checkUpkeep("");
if(upkeepNeeded) {
(uint128[] memory marketIds, address[] memory assets) = abi.decode(performData, (uint128[], address[]));
for(uint i; i<assets.length; i++) {
if(assets[i] !=address(0)) {
feeConversionAssets.push(assets[i]);
feeConversionMarketIds.push(marketIds[i]);
}
}
}
vm.prank(keeper);
FeeConversionKeeper(feeConversionKeeper).performUpkeep(abi.encode(feeConversionMarketIds, feeConversionAssets)) ;
console2.log("This is the earned fees after swap fees %d",marketMakingEngine.workaround_getPendingProtocolWethReward(3)+marketMakingEngine.getEarnedFees(8,0x0000000000000000000000000000000000010000)+marketMakingEngine.getEarnedFees(14,0x0000000000000000000000000000000000010000));
console2.log("This is the balance after swap fees : %d",wEth.balanceOf(address(marketMakingEngine)));
vm.prank(bob);
marketMakingEngine.claimFees(14);
PerpMarketCreditConfig memory currentPerpMarketCreditConfig = perpMarketsCreditConfig[uint256(3)];
vm.prank(currentPerpMarketCreditConfig.engine);
marketMakingEngine.sendWethToFeeRecipients(3);
vm.prank(bob);
marketMakingEngine.claimFees(8);
}

Impact

fees will not be claimable.

Tools Used

Echidna.

Recommendations

Change the logic of the function to handler rounding issues

Updates

Lead Judging Commences

inallhonesty Lead Judge
6 months ago
inallhonesty Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

0xphantom Submitter
6 months ago
inallhonesty Lead Judge
5 months ago
inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Rounding errors in _handleWethRewardDistribution cause overestimation of available rewards leading to protocol insolvency when users claim fees tag.

Support

FAQs

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