The Standard

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

Distributing assets with less than 18 decimals, may result in smaller stakers not receiving rewards

Summary

The protocol has mentioned that they are goin to use WBTC as a collateral token, WBTC has 8 decimals. uint256 _portion = asset.amount * _positionStake / stakeTotal; this is how the initial portion of the rewards that a user should receive is calculated when an undercollateralized vault is lliquidated. If there are a lot of stakes in the LiquidationPool.sol contract lets say 100_000_000e18 we get 1e26 in the denominator. Now lets say the user has staked 99e18 EURO or because of previously received rewards his stake is now below 100e18. And we have liquidated a vault with 993619 WBTC tokens then we will get the following calculation 99e18 * 993619 = 9.8368281e+25, then when we divide it by 1e26 we get the following (99e18 * 993619) / 1e26= 0.98368281 but due to solidity not supporting floating numbers out of the box and rounding down when dividing we will get 0. Although the user should have received some small portion of the rewards he won't receive them. If there is one user with a stake of less than 100e18 this is not a big problem. However if there are 100_000 users with such stakes this may result in a problem for the protocol.
Keep in mind that when users receives rewards their EURO position is decreasing, when it comes to a certain level they will also become not eligible for rewards.

Note: The problem is even bigger if the token has 6 decimals for example USDC which the team has mentioned they may add as collateral in later stages of the protocol development.

Vulnerability Details

Gist

After executing the steps provided in the above gist in order to set up the tests, add the following functions to the AuditorTests.t.sol

function setUpLiquidityPool() public {
skip(2 days);
vm.startPrank(alice);
(, int256 EurUsdRate,,,) = ClEurUsd.latestRoundData();
(, int256 EthUsdRate,,,) = ClEthUsd.latestRoundData();
uint256 amountInETH = (65_000_000e18 * uint256(EurUsdRate)) / uint256(EthUsdRate);
vm.deal(alice, amountInETH);
(address vault, ) = vaultManagerV5Instance.mint();
address(vault).call{value: amountInETH}("");
SmartVaultV3 vaultInstance = SmartVaultV3(payable(vault));
uint256 maxMintable = vaultInstance.maxMintable();
uint256 fee = maxMintable * vaultManagerV5Instance.mintFeeRate() / vaultManagerV5Instance.HUNDRED_PC();
uint256 amountToMint = maxMintable - fee;
console2.log("Here is maxMintable and amountToMint: ", maxMintable, amountToMint);
vaultInstance.mint(alice, amountToMint);
TST.mint(alice, EURO.balanceOf(alice));
TST.approve(address(liquidationPool), TST.balanceOf(alice));
EURO.approve(address(liquidationPool), EURO.balanceOf(alice));
console2.log("Alice EURO balance: ", EURO.balanceOf(alice));
liquidationPool.increasePosition(TST.balanceOf(alice), EURO.balanceOf(alice));
vm.stopPrank();
vm.startPrank(bob);
vm.deal(bob, amountInETH);
(address vaultB, ) = vaultManagerV5Instance.mint();
address(vaultB).call{value: amountInETH}("");
SmartVaultV3 vaultInstanceB = SmartVaultV3(payable(vaultB));
console2.log("Here is maxMintable and amountToMint: ", maxMintable, amountToMint);
vaultInstanceB.mint(bob, amountToMint);
TST.mint(bob, EURO.balanceOf(bob));
TST.approve(address(liquidationPool), TST.balanceOf(bob));
EURO.approve(address(liquidationPool), EURO.balanceOf(bob));
console2.log("Alice EURO balance: ", EURO.balanceOf(bob));
liquidationPool.increasePosition(TST.balanceOf(bob), EURO.balanceOf(bob));
vm.stopPrank();
vm.startPrank(john);
uint256 amountInETHJohn = (100e18 * uint256(EurUsdRate)) / uint256(EthUsdRate);
vm.deal(john, amountInETH);
(address vaultJ, ) = vaultManagerV5Instance.mint();
address(vaultJ).call{value: amountInETHJohn}("");
SmartVaultV3 vaultInstanceJ = SmartVaultV3(payable(vaultJ));
uint256 maxMintableJ = vaultInstanceJ.maxMintable();
uint256 feeJ = maxMintableJ * vaultManagerV5Instance.mintFeeRate() / vaultManagerV5Instance.HUNDRED_PC();
uint256 amountToMintJ = maxMintableJ - feeJ;
vaultInstanceJ.mint(john, amountToMintJ);
TST.mint(john, EURO.balanceOf(john));
TST.approve(address(liquidationPool), type(uint256).max);
EURO.approve(address(liquidationPool), EURO.balanceOf(john));
console2.log("Balance of John: ", EURO.balanceOf(john));
liquidationPool.increasePosition(TST.balanceOf(john), EURO.balanceOf(john));
/// @dev skip 2 days and call increasePosition once more wiht a minimum deposit in order to transfrom the pending stakes to positions
skip(2 days);
TST.mint(john, 1);
liquidationPool.increasePosition(1, 0);
vm.stopPrank();
}
function test_MissedRewardsWithTokensWithLessDecimals() public {
setUpLiquidityPool();
vm.startPrank(tom);
(, int256 EurUsdRate,,,) = ClEurUsd.latestRoundData();
(, int256 WbtcUsdRate,,,) = ClWbtcUsd.latestRoundData();
uint256 amountInWBTCTom = (400e8 * uint256(EurUsdRate)) / uint256(WbtcUsdRate);
WBTC.mint(tom, amountInWBTCTom);
(address vaultT, uint256 vaultId) = vaultManagerV5Instance.mint();
WBTC.transfer(address(vaultT), amountInWBTCTom);
SmartVaultV3 vaultInstanceT = SmartVaultV3(payable(vaultT));
uint256 maxMintableT = vaultInstanceT.maxMintable();
uint256 feeT = maxMintableT * vaultManagerV5Instance.mintFeeRate() / vaultManagerV5Instance.HUNDRED_PC();
uint256 amountToMintT = maxMintableT - feeT;
vaultInstanceT.mint(tom, amountToMintT);
console2.log("Tom EUROs balance: ", EURO.balanceOf(tom));
console2.log("Tom euros balance divided: ", EURO.balanceOf(tom)/1e18);
console2.log("VaultT WBTC balance: ", WBTC.balanceOf(vaultT));
vm.stopPrank();
vm.startPrank(attacker);
console2.log("Here is the Euro balance of the liquidationPool contract: ", EURO.balanceOf(address(liquidationPool)));
console2.log("Here is the Euro balance of the liquidationPool contract divided by 1e18: ", EURO.balanceOf(address(liquidationPool))/1e18);
/// @notice we drop the price of WBTC
console2.log("Balance of Tom's vault in EUROs before we drop the price: ", vaultInstanceT.euroCollateral()/1e18);
ClWbtcUsd.setPrice(4311586000000);
console2.log("Balance of Tom's vault in EUROs before we drop the price: ", vaultInstanceT.euroCollateral()/1e18);
(, uint256 TSTa, uint256 EUROa) = liquidationPool.positions(alice);
(, uint256 TSTb, uint256 EUROb) = liquidationPool.positions(bob);
(, uint256 TSTj, uint256 EUROj) = liquidationPool.positions(john);
console2.log("Position of alice, bob and john: ", EUROa/1e18, EUROb/1e18, EUROj/1e18);
liquidationPoolManager.runLiquidation(vaultId);
(bytes32 symbol, address addr, uint8 dec, address clAddr, uint8 clDec) = tokenManager.acceptedTokens(1);
bytes memory aliceRewards = abi.encodePacked(alice, symbol);
bytes memory bobRewards = abi.encodePacked(bob, symbol);
bytes memory johnRewards = abi.encodePacked(john, symbol);
console2.log("Alice acumulated rewards in WBTC: ", liquidationPool.rewards(aliceRewards));
console2.log("Bob acumulated rewards in WBTC: ", liquidationPool.rewards(bobRewards));
console2.log("John acumulated rewards in WBTC: ", liquidationPool.rewards(johnRewards));
vm.stopPrank();
}
Logs:
Here is maxMintable and amountToMint: 54166666666666666666665265 53895833333333333333331939
Alice EURO balance: 53895833333333333333331939
Here is maxMintable and amountToMint: 54166666666666666666665265 53895833333333333333331939
Alice EURO balance: 53895833333333333333331939
Balance of John: 82916666666666665398
Tom EUROs balance: 331666339023334032936
Tom euros balance divided: 331
VaultT WBTC balance: 993619
Here is the Euro balance of the liquidationPool contract: 107926489373958333333329268
Here is the Euro balance of the liquidationPool contract divided by 1e18: 107926489
Balance of Tom's vault in EUROs before we drop the price: 399
Balance of Tom's vault in EUROs before we drop the price: 390
Position of alice, bob and john: 54030573 53895833 82
Alice acumulated rewards in WBTC: 496809
Bob acumulated rewards in WBTC: 496809
John acumulated rewards in WBTC: 0

To run the test use: forge test -vvv --mt test_MissedRewardsWithTokensWithLessDecimals

Impact

Users with small stakes in the LiquidationPool.sol won't receive rewards from a liquidated smart vault.

Tools Used

Manual Review & Foundry

Recommendations

Either consider improving the math in the distributeAssets() function or request a minimum amount of stakes for users, something of the sort of 1000e18 EURO

Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

precision

Support

FAQs

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