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));
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:
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:
Use this code where ConsolidateStakes
contract simulates the minimum requirements to run this function, and ConsolidaTest
represents the test contract.
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));
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)