The Standard

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

[H-#] ARITHMETIC LOGIC ISSUE IN LIQUIDATION POOL, MAKES IT IMPOSSIBLE TO MODIFY POSITIONS OR DISTRIBUTE ASSETS

Description: The LiquidationPool::consolidatePendingStakes() function throws an arithmetic underflow/overflow error if the initial users of the LiquidationPool::pendingStakes array are in conditions of consolidating their positions. Check the function here

function consolidatePendingStakes() private {
uint256 deadline = block.timestamp - 1 days;
@> for (int256 i = 0; uint256(i) < pendingStakes.length; i++) {
PendingStake memory _stake = pendingStakes[uint256(i)];
if (_stake.createdAt < deadline) {
positions[_stake.holder].holder = _stake.holder;
positions[_stake.holder].TST += _stake.TST;
positions[_stake.holder].EUROs += _stake.EUROs;
deletePendingStake(uint256(i));
// pause iterating on loop because there has been a deletion. "next" item has same index
i--;
}
}
}

If pendingStakes[uint256(0)] meets the condition pendingStakes[uint256(0)].createdAt < (block.timestamp - 1 days), it triggers the if statement. If pendingStakes[uint256(0)] is deleted, this results in the replacement of i = 0 with i = -1, causing an arithmetic issue on the next iteration when the compiler attempts to process uint256(-1).

Impact: This error is highly probable, given the short wait time of users (besides the logical error). When it occurs, users will be unable to execute the following functions:

  • LiquidationPool::decreasePosition()

  • LiquidationPool::increasePosition()

  • LiquidationPool::distributeAssets()
    This leads to a severe disruption in the protocol's functionality and availability.

Proof of Concept: Two methods demonstrate this bug:

1- With Remix: Get in Remix and paste the following contract:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract TestingIntegers {
function checkNegativeValue(int256 value) public pure returns (uint256) {
uint256 newVal = uint256(value);
return newVal;
}
}

Compile, deploy, and interact with the checkNegativeValue() function, passing -1 as a parameter. You'll receive 0: uint256: 115792089237316195423570985008687907853269984665640564039457584007913129639935 as an output.

2- Foundry Test: I created a simple test file that emulates this issue after initializing your Foundry project with:

$ forge init

Use this code where ConsolidateStakes contract simulates the minimum requirements to run this function, and ConsolidaTest represents the test contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import {Test, console} from "forge-std/Test.sol";
contract ConsolidateStakes {
uint256 someValue;
struct Position {
address holder;
uint256 TST;
uint256 EUROs;
}
struct PendingStake {
address holder;
uint256 createdAt;
uint256 TST;
uint256 EUROs;
}
mapping(address => Position) private positions;
PendingStake[] private pendingStakes;
function deletePendingStake(uint256 _i) private {
for (uint256 i = _i; i < pendingStakes.length - 1; i++) {
pendingStakes[i] = pendingStakes[i + 1];
}
pendingStakes.pop();
}
function consolidatePendingStakes() public {
uint256 deadline = block.timestamp - 1 days;
for (int256 i = 0; uint256(i) < pendingStakes.length; i++) {
PendingStake memory _stake = pendingStakes[uint256(i)];
if (_stake.createdAt < deadline) {
positions[_stake.holder].holder = _stake.holder;
positions[_stake.holder].TST += _stake.TST;
positions[_stake.holder].EUROs += _stake.EUROs;
deletePendingStake(uint256(i));
// pause iterating on loop because there has been a deletion. "next" item has same index
i--;
}
}
}
function createPendingStakeStruct(
address _holder,
uint256 _createdAt,
uint256 _tst,
uint256 _euros
) public returns (PendingStake memory) {
return PendingStake(_holder, _createdAt, _tst, _euros);
}
function createAndPushPendingStake(
address _holder,
uint256 _createdAt,
uint256 _tst,
uint256 _euros
) external {
PendingStake memory _pStake = createPendingStakeStruct(
_holder,
_createdAt,
_tst,
_euros
);
pendingStakes.push(_pStake);
}
function getPendingStakeLength() external view returns (uint256) {
return pendingStakes.length;
}
}
contract consolidaTest is Test {
ConsolidateStakes conStakes;
function setUp() public {
conStakes = new ConsolidateStakes();
}
function testConsolidatingFunction(uint160 _indexValue) public {
vm.assume(_indexValue < 20);
vm.warp(block.timestamp);
for (uint160 i; i < _indexValue; i++) {
address user;
if (i == 0) {
user = address(i + 1);
} else {
user = address(i);
}
conStakes.createAndPushPendingStake(
user,
block.timestamp,
1000,
200
);
}
vm.warp(block.timestamp + 30000);
conStakes.consolidatePendingStakes();
}
}

Disclaimer: To manage computational costs, I've restricted the Fuzz test using vm.assume(_indexValue < 20);. Additionally, I've adjusted timestamps using vm.warp() to ensure pendingStakes[uint256(i)].createdAt < (block.timestamp - 1 days) is met.

To run this test on your console, execute:

$ forge test --mt testConsolidatingFunction -vvvvv

The test will demonstrate a panic (0x11) error:

Failing tests:
Encountered 1 failing test in test/consolidatingTest.t.sol:consolidaTest
[FAIL. Reason: panic: arithmetic underflow or overflow (0x11); counterexample: calldata=0xb3b80f430000000000000000000000000000000000000000000000000000000000000000 args=[0]] testConsolidatingFunction(uint160) (runs: 0, μ: 0, ~: 0)
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.