The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: low
Invalid

`LiquidationPoolManager::runLiquidation` will fail for tokens with more than 18 decimals

Summary

Vaults that have balances of NEAR tokens will be problematic during liquidations. NEAR tokens have decimals more than 18; precisely 24 that will lead to revert/underflow panic errors which will make liquidating such vaults impossible.

Vulnerability Details

The vulnerability lies in the calculations done in the distributeAssets function which is triggered when a liquidation of a vault is run in the runLiquidation() function of the LiquidationPoolManager contract:

FILE: LiquidationPool.sol
function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
consolidatePendingStakes();
(,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
uint256 stakeTotal = getStakeTotal();
uint256 burnEuros;
uint256 nativePurchased;
for (uint256 j = 0; j < holders.length; j++) {
Position memory _position = positions[holders[j]];
uint256 _positionStake = stake(_position);
if (_positionStake > 0) {
for (uint256 i = 0; i < _assets.length; i++) {
ILiquidationPoolManager.Asset memory asset = _assets[i];
if (asset.amount > 0) {
(,int256 assetPriceUsd,,,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();
uint256 _portion = asset.amount * _positionStake / stakeTotal;
@> uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd)
* _hundredPC / _collateralRate; // @audit-issue underflow issue with tokens that have more than 18 decimals
if (costInEuros > _position.EUROs) {
_portion = _portion * _position.EUROs / costInEuros;
costInEuros = _position.EUROs;
}
_position.EUROs -= costInEuros;
rewards[abi.encodePacked(_position.holder, asset.token.symbol)] += _portion;
burnEuros += costInEuros;
if (asset.token.addr == address(0)) {
nativePurchased += _portion;
} else {
IERC20(asset.token.addr).safeTransferFrom(manager, address(this), _portion);
}
}
}
}
positions[holders[j]] = _position;
}
if (burnEuros > 0) IEUROs(EUROs).burn(address(this), burnEuros);
returnUnpurchasedNative(_assets, nativePurchased);
}

POC

POC setup link to gist: https://gist.github.com/rexjoseph/88bcf096b30e4abfbf9d00e8cc590209

function testRunLiquidationWithNEARPOC() public {
ILiquidationPoolManager.Asset[] memory _assets;
uint256 collateralRate = DEFAULT_COLLATERAL_RATE;
uint256 hundredPc = 100000;
// enter positions
uint256 balance = 5000 ether;
uint256 tstVal = 1000 ether;
uint256 eurosVal = 500 ether;
TST.mint(user1, balance);
EUROs.mint(user1, balance);
vm.prank(user1);
vm.expectRevert();
liquidationPool.increasePosition(tstVal, eurosVal);
(LiquidationPool.Position memory position, LiquidationPool.Reward[] memory rewards) = liquidationPool.position(user1);
assert(position.TST == 0);
assert(position.EUROs == 0);
// Approve the LiquidationPool to spend user1's tokens
vm.startPrank(user1);
TST.approve(address(liquidationPool), tstVal);
EUROs.approve(address(liquidationPool), eurosVal);
vm.stopPrank();
// increasePosition
vm.prank(user1);
liquidationPool.increasePosition(tstVal, eurosVal);
(LiquidationPool.Position memory positionOne, LiquidationPool.Reward[] memory rewardsOne) = liquidationPool.position(user1);
vm.startPrank(user1);
uint256 mockValue = 500e24;
NEAR.mint(user1, mockValue);
NEAR.transfer(address(mockSmartVaultManager), mockValue);
uint256 balOfMSVM = NEAR.balanceOf(address(mockSmartVaultManager));
assertEq(balOfMSVM, mockValue);
vm.warp(block.timestamp + 2 days);
liquidationPoolManager.runLiquidation(0);
}

Impact

This results in reverts of such transactions to liquidate vaults that have thier balances in NEAR. Such vaults are then forced to be open and remain undercollateralized for as long as possible since the tokens decimals will keep causing the issue of underflow.

This is the result of a coded POC to attempt liquidation of such vaults:

└─ ← true
│ ├─ [286] MockSmartVaultManager::collateralRate() [staticcall]
│ │ └─ ← 120000 [1.2e5]
│ ├─ [220] MockSmartVaultManager::HUNDRED_PC() [staticcall]
│ │ └─ ← 100000 [1e5]
│ ├─ [72320] LiquidationPool::distributeAssets([Asset({ token: Token({ symbol: 0x0000000000000000000000000000000000000000000000000000000000000000, addr: 0x0000000000000000000000000000000000000000, dec: 0, clAddr: 0x0000000000000000000000000000000000000000, clDec: 0 }), amount: 0 }), Asset({ token: Token({ symbol: 0x0000000000000000000000000000000000000000000000000000000000000000, addr: 0x0000000000000000000000000000000000000000, dec: 0, clAddr: 0x0000000000000000000000000000000000000000, clDec: 0 }), amount: 0 }), Asset({ token: Token({ symbol: 0x0000000000000000000000000000000000000000000000000000000000000000, addr: 0x0000000000000000000000000000000000000000, dec: 0, clAddr: 0x0000000000000000000000000000000000000000, clDec: 0 }), amount: 0 }), Asset({ token: Token({ symbol: 0x4e45415200000000000000000000000000000000000000000000000000000000, addr: 0xa0Cb889707d426A7A386870A03bc70d1b0697598, dec: 24, clAddr: 0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF, clDec: 8 }), amount: 500000000000000000000000000 [5e26] })], 120000 [1.2e5], 100000 [1e5])
│ │ ├─ [7148] ChainlinkMock::latestRoundData() [staticcall]
│ │ │ └─ ← 0, 106000000 [1.06e8], 0, 1641056400 [1.641e9], 0
│ │ ├─ [7148] ChainlinkMock::latestRoundData() [staticcall]
│ │ │ └─ ← 0, 304000000 [3.04e8], 0, 1641056400 [1.641e9], 0
│ │ └─ ← panic: arithmetic underflow or overflow (0x11)
│ └─ ← panic: arithmetic underflow or overflow (0x11)
└─ ← panic: arithmetic underflow or overflow (0x11)
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 7.77ms
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/foundry/unit/LiquidationPool.t.sol:LiquidationPoolTest
[FAIL. Reason: panic: arithmetic underflow or overflow (0x11)] testRunLiquidationWithNEARPOC() (gas: 787409)
Encountered a total of 1 failing tests, 0 tests succeeded

Tools Used

Manual review

Recommendations

  1. If you intend to make this not revert, you will need to assume token decimals of assets can be more than 18 and handle such a case where it is.

  2. Rather than doing _portion * 10 ** (18 - asset.token.dec) DO _portion * 10 ** (18 * uint256(assetPriceUsd) / uint256(priceEurUsd) / 10 ** asset.token.dec)

Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

informational/invalid

Support

FAQs

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